From f9e8ee5e698e021310b3c66becae14dacd49b4f1 Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Sun, 20 Dec 2020 13:11:31 +0000 Subject: [PATCH 01/33] Initial commit --- GitHubBranches.ps1 | 882 +++++++++++++++++++++++++++++++++++++-- GitHubCore.ps1 | 310 ++++++++++++++ PowerShellForGitHub.psd1 | 4 + 3 files changed, 1158 insertions(+), 38 deletions(-) diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 index 969f7264..d786f8a0 100644 --- a/GitHubBranches.ps1 +++ b/GitHubBranches.ps1 @@ -597,6 +597,162 @@ filter Get-GitHubRepositoryBranchProtectionRule return (Invoke-GHRestMethod @params | Add-GitHubBranchProtectionRuleAdditionalProperties) } +filter Get-GitHubQlRepositoryBranchProtectionRule +{ + <# + .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 BranchNamePattern + Name of the specific branch Pattern to be retrieved. If not supplied, all rules 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. + + .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-GitHubQlRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchNamePattern master + + Retrieves branch protection rules for the master branch of the PowerShellForGithub repository. + + .EXAMPLE + Get-GitHubQlRepositoryBranchProtectionRule -Uri 'https://github.com/microsoft/PowerShellForGitHub' -BranchNamePattern 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="The Uri parameter is only referenced by Resolve-RepositoryElements 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)] + [Alias('BranchName')] + [string] $BranchNamePattern, + + [string] $AccessToken + ) + + Write-InvocationLog + + $elements = Resolve-RepositoryElements + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $branchProtectionRuleFields = 'allowsDeletions allowsForcePushes dismissesStaleReviews id ' + + 'isAdminEnforced pattern requiredApprovingReviewCount requiredStatusCheckContexts ' + + 'requiresApprovingReviews requiresCodeOwnerReviews requiresCommitSignatures requiresLinearHistory ' + + 'requiresStatusChecks requiresStrictStatusChecks restrictsPushes restrictsReviewDismissals ' + + 'repository { url }' + + $hashbody = @{query = "query branchProtectionRule { repository(name: ""$RepositoryName"", " + + "owner: ""$OwnerName"") { branchProtectionRules(first: 100) { nodes { " + + " $branchProtectionRuleFields } } } }"} + + $description = "Querying $RepositoryName repository for branch protection rules" + + Write-Debug -Message $description + Write-Debug -Message "Query: $($hashbody.query)" + + $params = @{ + Body = ConvertTo-Json -InputObject $hashBody + Description = $description + AccessToken = $AccessToken + TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryProperties = $telemetryProperties + } + + $result = Invoke-GHGraphQl @params + + if ($result -is [System.Management.Automation.ErrorRecord]) + { + $PSCmdlet.ThrowTerminatingError($result) + } + + if ($result.data.repository.branchProtectionRules) + { + $rule = ($result.data.repository.branchProtectionRules.nodes | + Where-Object -Property pattern -eq $BranchNamePattern) + } + + if (!$rule) + { + $exception = [Exception]::new( + "Branch Protection Rule '$BranchNamePattern' not found on repository $RepositoryName") + $errorRecord = [System.Management.Automation.ErrorRecord]::new( + $exception, + 'BranchProtectionRuleNotFound', + [System.Management.Automation.ErrorCategory]::ObjectNotFound, + $BranchNamePattern + ) + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + #return ($rule | Add-GitHubBranchProtectionRuleAdditionalProperties) + return $rule + +} + filter New-GitHubRepositoryBranchProtectionRule { <# @@ -950,14 +1106,14 @@ filter New-GitHubRepositoryBranchProtectionRule return (Invoke-GHRestMethod @params | Add-GitHubBranchProtectionRuleAdditionalProperties) } -filter Remove-GitHubRepositoryBranchProtectionRule +filter New-GitHubQLRepositoryBranchProtectionRule { <# .SYNOPSIS - Remove branch protection rules from a given GitHub repository. + Creates a branch protection rule for a branch on a given GitHub repository. .DESCRIPTION - Remove branch protection rules from a given GitHub repository. + 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 @@ -974,8 +1130,61 @@ filter Remove-GitHubRepositoryBranchProtectionRule 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 OrganizationName + Name of the Organization. + + .PARAMETER BranchNamePattern + The branch name pattern 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 RequireSignedCommits + + .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 @@ -986,31 +1195,28 @@ filter Remove-GitHubRepositoryBranchProtectionRule GitHub.Branch .OUTPUTS - None - - .EXAMPLE - Remove-GitHubRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchName master + GitHub.BranchRepositoryRule - Removes branch protection rules from the master branch of the PowerShellForGithub repository. + .NOTES + Protecting a branch requires admin or owner permissions to the repository. .EXAMPLE - Removes-GitHubRepositoryBranchProtection -Uri 'https://github.com/microsoft/PowerShellForGitHub' -BranchName master + New-GitHubRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchName master -EnforceAdmins - Removes branch protection rules from the master branch of the PowerShellForGithub repository. + Creates a branch protection rule for the master branch of the PowerShellForGithub repository + enforcing all configuration restrictions for administrators. .EXAMPLE - Removes-GitHubRepositoryBranchProtection -Uri 'https://github.com/master/PowerShellForGitHub' -BranchName master -Force + New-GitHubRepositoryBranchProtectionRule -Uri 'https://github.com/microsoft/PowerShellForGitHub' -BranchName master -RequiredApprovingReviewCount 1 - Removes branch protection rules from the master branch of the PowerShellForGithub repository - without prompting for confirmation. + Creates a branch protection rule for the master branch of the PowerShellForGithub repository + requiring one approving review. #> [CmdletBinding( PositionalBinding = $false, SupportsShouldProcess, - DefaultParameterSetName = 'Elements', - ConfirmImpact = "High")] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "", Justification="The Uri parameter is only referenced by Resolve-RepositoryElements which get access to it from the stack via Get-Variable -Scope 1.")] - [Alias('Delete-GitHubRepositoryBranchProtectionRule')] + DefaultParameterSetName = 'Elements')] + [OutputType({$script:GitHubBranchProtectionRuleTypeName })] param( [Parameter(ParameterSetName = 'Elements')] [string] $OwnerName, @@ -1020,19 +1226,51 @@ filter Remove-GitHubRepositoryBranchProtectionRule [Parameter( Mandatory, - Position = 1, ValueFromPipelineByPropertyName, + Position = 1, ParameterSetName = 'Uri')] [Alias('RepositoryUrl')] [string] $Uri, + [string] $OrganizationName, + [Parameter( Mandatory, ValueFromPipelineByPropertyName, Position = 2)] - [string] $BranchName, + [Alias('BranchName')] + [string] $BranchNamePattern, - [switch] $Force, + [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, + + [switch] $RequireSignedCommits, [string] $AccessToken ) @@ -1043,33 +1281,601 @@ filter Remove-GitHubRepositoryBranchProtectionRule $OwnerName = $elements.ownerName $RepositoryName = $elements.repositoryName - $telemetryProperties = @{ - 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) - 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) - } - - if ($Force -and (-not $Confirm)) + If ([System.String]::IsNullOrEmpty($OrganizationName)) { - $ConfirmPreference = 'None' + $OrganizationName = $OwnerName } - if (-not $PSCmdlet.ShouldProcess("'$BranchName' branch of repository '$RepositoryName'", - 'Remove GitHub Repository Branch Protection Rule')) - { - return + $telemetryProperties = @{ + OwnerName = (Get-PiiSafeString -PlainText $OwnerName) + RepositoryName = (Get-PiiSafeString -PlainText $RepositoryName) } + $hashbody = @{query = "query repo { repository(name: ""$RepositoryName"" , " + + "owner: ""$OwnerName"") { id } }"} + + Write-Debug -Message "Querying Repository $RepositoryName, Owner $OwnerName" + $params = @{ - UriFragment = "repos/$OwnerName/$RepositoryName/branches/$BranchName/protection" - Description = "Removing $BranchName branch protection rule for $RepositoryName" - Method = 'Delete' - AcceptHeader = $script:lukeCageAcceptHeader + Body = ConvertTo-Json -InputObject $hashBody + Description = "Querying $RepositoryName" AccessToken = $AccessToken TelemetryEventName = $MyInvocation.MyCommand.Name TelemetryProperties = $telemetryProperties } - return Invoke-GHRestMethod @params | Out-Null + $result = Invoke-GHGraphQl @params + + if ($result -is [System.Management.Automation.ErrorRecord]) + { + $PSCmdlet.ThrowTerminatingError($result) + } + + $repoId = $result.data.repository.id + + $mutationList = @( + "repositoryId: ""$repoId"", pattern: ""$BranchNamePattern""" + 'requiresLinearHistory: ' + $RequireLinearHistory.ToBool().ToString().ToLower() + 'allowsForcePushes: ' + $AllowForcePushes.ToBool().ToString().ToLower() + 'allowsDeletions: ' + $AllowDeletions.ToBool().ToString().ToLower() + 'isAdminEnforced: ' + $EnforceAdmins.ToBool().ToString().ToLower() + 'dismissesStaleReviews: ' + $DismissStaleReviews.ToBool().ToString().ToLower() + 'requiresCodeOwnerReviews: ' + $RequireCodeOwnerReviews.ToBool().ToString().ToLower() + 'requiresStrictStatusChecks: ' + $RequireUpToDateBranches.ToBool().ToString().ToLower() + 'requiresCommitSignatures: ' + $RequireSignedCommits.ToBool().ToString().ToLower() + ) + + if ($PSBoundParameters.ContainsKey('RequiredApprovingReviewCount')) + { + $mutationList += 'requiresApprovingReviews: true' + $mutationList += 'requiredApprovingReviewCount: ' + $RequiredApprovingReviewCount + } + if ($PSBoundParameters.ContainsKey('StatusChecks')) + { + $mutationList += 'requiresStatusChecks: true' + $mutationList += 'requiredStatusCheckContexts: [ "' + ($StatusChecks -join ('","')) + '" ]' + } + + If ($PSBoundParameters.ContainsKey('RestrictPushUsers') -or + $PSBoundParameters.ContainsKey('RestrictPushTeams') -or + $PSBoundParameters.ContainsKey('RestrictPushApps')) + { + $restrictPushActorIds = @() + + If ($PSBoundParameters.ContainsKey('RestrictPushUsers')) + { + Foreach($user in $RestrictPushUsers) + { + $hashbody = @{query = "query user { user(login: ""$user"") { id } }"} + + $description = "Querying User $user" + Write-Debug -Message $description + + $params = @{ + Body = ConvertTo-Json -InputObject $hashBody + Description = $description + AccessToken = $AccessToken + TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryProperties = $telemetryProperties + } + + $result = Invoke-GHGraphQl @params + + if ($result -is [System.Management.Automation.ErrorRecord]) + { + $PSCmdlet.ThrowTerminatingError($result) + } + + $restrictPushActorIds += $result.data.user.id + } + } + + If ($PSBoundParameters.ContainsKey('RestrictPushTeams')) + { + Foreach($team in $RestrictPushTeams) + { + $hashbody = @{query = "query organization { organization(login: ""$OrganizationName"") " + + "{ team(slug: ""$team"") { id } } }"} + + $description = "Querying $OrganizationName organisation for team $team" + + Write-Debug -Message $description + + $params = @{ + Body = ConvertTo-Json -InputObject $hashBody + Description = $description + AccessToken = $AccessToken + TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryProperties = $telemetryProperties + } + + $result = Invoke-GHGraphQl @params + if ($result -is [System.Management.Automation.ErrorRecord]) + { + $PSCmdlet.ThrowTerminatingError($result) + } + + if ($result.data.organization.team) + { + $restrictPushActorIds += $result.data.organization.team.id + } + else + { + $exception = [Exception]::new("Team $team not found in organization $OrganizationName") + $errorRecord = [System.Management.Automation.ErrorRecord]::new( + $exception, + 'RestictPushTeamNotFound', + [System.Management.Automation.ErrorCategory]::ObjectNotFound, + $team + ) + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + } + } + + if ($PSBoundParameters.ContainsKey('RestrictPushApps')) + { + Foreach($app in $RestrictPushApps) + { + $hashbody = @{query = "query app { marketplaceListing(slug: ""$app"") { app { id } } }"} + + $description = "Querying for app $app" + + Write-Debug -Message $description + + $params = @{ + Body = ConvertTo-Json -InputObject $hashBody + Description = $description + AccessToken = $AccessToken + TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryProperties = $telemetryProperties + } + + $result = Invoke-GHGraphQl @params + + if ($result -is [System.Management.Automation.ErrorRecord]) + { + $PSCmdlet.ThrowTerminatingError($result) + } + + if ($result.data.marketplaceListing) + { + $restrictPushActorIds += $result.data.marketplaceListing.app.id + } + else + { + $exception = [Exception]::new("App $app not found") + $errorRecord = [System.Management.Automation.ErrorRecord]::new( + $exception, + 'RestictPushAppNotFound', + [System.Management.Automation.ErrorCategory]::ObjectNotFound, + $team + ) + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + } + } + + $mutationList += 'restrictsPushes: true' + $mutationList += 'pushActorIds: [ "' + ($restrictPushActorIds -join ('","')) + '" ]' + } + + if ($PSBoundParameters.ContainsKey('DismissalUsers') -or + $PSBoundParameters.ContainsKey('DismissalTeams')) + { + $reviewDismissalActorIds = @() + + If ($PSBoundParameters.ContainsKey('DismissalUsers')) + { + Foreach($user in $DismissalUsers) + { + $hashbody = @{query = "query user { user(login: ""$user"") { id } }"} + + $description = "Querying user $user" + + Write-Debug -Message $description + + $params = @{ + Body = ConvertTo-Json -InputObject $hashBody + Description = $description + AccessToken = $AccessToken + TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryProperties = $telemetryProperties + } + + $result = Invoke-GHGraphQl @params + + if ($result -is [System.Management.Automation.ErrorRecord]) + { + $PSCmdlet.ThrowTerminatingError($result) + } + + $reviewDismissalActorIds += $result.data.user.id + } + } + + If ($PSBoundParameters.ContainsKey('DismissalTeams')) + { + Foreach($team in $DismissalTeams) + { + $hashbody = @{query = "query organization { organization(login: ""$OrganizationName"") " + + "{ team(slug: ""$team"") { id } } }"} + + $description = "Querying $OrganizationName organisation for team $team" + + Write-Debug -Message $description + + $params = @{ + Body = ConvertTo-Json -InputObject $hashBody + Description = $description + AccessToken = $AccessToken + TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryProperties = $telemetryProperties + } + + $result = Invoke-GHGraphQl @params + + if ($result -is [System.Management.Automation.ErrorRecord]) + { + $PSCmdlet.ThrowTerminatingError($result) + } + + if ($result.data.organization.team) + { + $reviewDismissalActorIds += $result.data.organization.team.id + } + else + { + $exception = [Exception]::new("Team $team not found in organization $OrganizationName") + $errorRecord = [System.Management.Automation.ErrorRecord]::new( + $exception, + 'DismissalTeamNotFound', + [System.Management.Automation.ErrorCategory]::ObjectNotFound, + $team + ) + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + } + } + + $mutationList += 'restrictsReviewDismissals: true' + $mutationList += 'reviewDismissalActorIds: [ "' + ($reviewDismissalActorIds -join ('","')) + '" ]' + } + + $mutationInput = $mutationList -join(',') + $hashbody = @{query = "mutation ProtectionRule { createBranchProtectionRule(input: { $mutationInput }) " + + "{ clientMutationId } } " } + + $description = "Setting $BranchNamePattern branch protection status for $RepositoryName" + $body = ConvertTo-Json -InputObject $hashBody + + Write-Debug -Message $description + Write-Debug -Message "Query: $body" + + if (-not $PSCmdlet.ShouldProcess( + "Owner '$OwnerName', Repository '$RepositoryName'", + "Create '$BranchNamePattern' branch pattern GitHub Repository Branch Protection Rule")) + { + return + } + + $params = @{ + Body = $body + Description = $description + AccessToken = $AccessToken + TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryProperties = $telemetryProperties + } + + $result = Invoke-GHGraphQl @params + + if ($result -is [System.Management.Automation.ErrorRecord]) + { + $PSCmdlet.ThrowTerminatingError($result) + } +} + +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. + + .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="The Uri parameter is only referenced by Resolve-RepositoryElements 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 + ) + + 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 (-not $PSCmdlet.ShouldProcess("'$BranchName' branch of repository '$RepositoryName'", + 'Remove GitHub Repository Branch Protection Rule')) + { + return + } + + $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 + } + + return Invoke-GHRestMethod @params | Out-Null +} + +filter Remove-GitHubQlRepositoryBranchProtectionRule +{ + <# + .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 BranchNamePattern + Name of the specific branch pattern 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. + + .INPUTS + GitHub.Repository + GitHub.Branch + + .OUTPUTS + None + + .EXAMPLE + Remove-GitHubQLRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchNamePattern master + + Removes branch protection rules from the master branch of the PowerShellForGithub repository. + + .EXAMPLE + Removes-GitHubQLRepositoryBranchProtection -Uri 'https://github.com/microsoft/PowerShellForGitHub' -BranchNamePattern master + + Removes branch protection rules from the master branch of the PowerShellForGithub repository. + + .EXAMPLE + Removes-GitHubQLRepositoryBranchProtection -Uri 'https://github.com/master/PowerShellForGitHub' -BranchNamePattern 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="The Uri parameter is only referenced by Resolve-RepositoryElements which get access to it from the stack via Get-Variable -Scope 1.")] + [Alias('Delete-GitHubQLRepositoryBranchProtectionRule')] + 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)] + [Alias('BranchName')] + [string] $BranchNamePattern, + + [switch] $Force, + + [string] $AccessToken + ) + + Write-InvocationLog + + $elements = Resolve-RepositoryElements + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $hashbody = @{query = "query branchProtectionRule { repository(name: ""$RepositoryName"", " + + "owner: ""$OwnerName"") { branchProtectionRules(first: 100) { nodes { id pattern } } } }"} + + $description = "Querying $RepositoryName repository for branch protection rules" + + Write-Debug -Message $description + + $params = @{ + Body = ConvertTo-Json -InputObject $hashBody + Description = $description + AccessToken = $AccessToken + TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryProperties = $telemetryProperties + } + + $result = Invoke-GHGraphQl @params + + if ($result -is [System.Management.Automation.ErrorRecord]) + { + $PSCmdlet.ThrowTerminatingError($result) + } + + if ($result.data.repository.branchProtectionRules) + { + $ruleId = ($result.data.repository.branchProtectionRules.nodes | + Where-Object -Property pattern -eq $BranchNamePattern).id + } + + if (!$ruleId) + { + $exception = [Exception]::new( + "Branch Protection Rule '$BranchNamePattern' not found on repository $RepositoryName") + $errorRecord = [System.Management.Automation.ErrorRecord]::new( + $exception, + 'BranchProtectionRuleNotFound', + [System.Management.Automation.ErrorCategory]::ObjectNotFound, + $BranchNamePattern + ) + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + if ($Force -and (-not $Confirm)) + { + $ConfirmPreference = 'None' + } + + $hashbody = @{query = "mutation ProtectionRule { deleteBranchProtectionRule(input: " + + "{ branchProtectionRuleId: ""$ruleId"" } ) { clientMutationId } }" } + + $description = "Removing $BranchNamePattern branch protection rule for $RepositoryName" + $body = ConvertTo-Json -InputObject $hashBody + + Write-Debug -Message $description + Write-Debug -Message "Query: $body" + + if (-not $PSCmdlet.ShouldProcess("'$BranchNamePattern' branch of repository '$RepositoryName'", + 'Remove GitHub Repository Branch Protection Rule')) + { + return + } + + $params = @{ + Body = $body + Description = $description + AccessToken = $AccessToken + TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryProperties = $telemetryProperties + } + + $result = Invoke-GHGraphQl @params + + if ($result -is [System.Management.Automation.ErrorRecord]) + { + $PSCmdlet.ThrowTerminatingError($result) + } } filter Add-GitHubBranchAdditionalProperties diff --git a/GitHubCore.ps1 b/GitHubCore.ps1 index 3f37c402..b3de8900 100644 --- a/GitHubCore.ps1 +++ b/GitHubCore.ps1 @@ -744,6 +744,316 @@ function Invoke-GHRestMethodMultipleResult } } +function Invoke-GHGraphQl +{ +<# + .SYNOPSIS + A wrapper around Invoke-WebRequest that understands the GitHub GraphQL API. + + .DESCRIPTION + A very heavy wrapper around Invoke-WebRequest that understands the GitHub QraphQL API. + It also understands how to parse and handle errors from the REST calls. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Description + A friendly description of the operation being performed for logging. + + .PARAMETER Body + This parameter forms the body of the request. It will be automatically + encoded to UTF8 and sent as Content Type: "application/json; charset=UTF-8" + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api as opposed to requesting a new one. + + .PARAMETER TelemetryEventName + If provided, the successful execution of this REST command will be logged to telemetry + using this event name. + + .PARAMETER TelemetryProperties + If provided, the successful execution of this REST command will be logged to telemetry + with these additional properties. This will be silently ignored if TelemetryEventName + is not provided as well. + + .PARAMETER TelemetryExceptionBucket + If provided, any exception that occurs will be logged to telemetry using this bucket. + It's possible that users will wish to log exceptions but not success (by providing + TelemetryEventName) if this is being executed as part of a larger scenario. If this + isn't provided, but TelemetryEventName *is* provided, then TelemetryEventName will be + used as the exception bucket value in the event of an exception. If neither is specified, + no bucket value will be used. + + .OUTPUTS + PSCustomObject + + .EXAMPLE + Invoke-GHGraphQl + + .NOTES + This wraps Invoke-WebRequest as opposed to Invoke-RestMethod because we want access + to the headers that are returned in the response, and Invoke-RestMethod drops those headers. +#> + [CmdletBinding()] + [OutputType([System.Management.Automation.ErrorRecord])] + param( + [string] $Description, + + [Parameter(Mandatory)] + [string] $Body, + + [string] $AccessToken, + + [string] $TelemetryEventName = $null, + + [hashtable] $TelemetryProperties = @{}, + + [string] $TelemetryExceptionBucket = $null + ) + + Invoke-UpdateCheck + + # Telemetry-related + $stopwatch = New-Object -TypeName System.Diagnostics.Stopwatch + $localTelemetryProperties = @{} + $TelemetryProperties.Keys | ForEach-Object { $localTelemetryProperties[$_] = $TelemetryProperties[$_] } + $errorBucket = $TelemetryExceptionBucket + if ([String]::IsNullOrEmpty($errorBucket)) + { + $errorBucket = $TelemetryEventName + } + + $stopwatch.Start() + + $hostName = $(Get-GitHubConfiguration -Name "ApiHostName") + + if ($hostName -eq 'github.com') + { + $url = "https://api.$hostName/graphql" + } + else + { + $url = "https://$hostName/api/v3/graphql" + } + + $headers = @{ + 'User-Agent' = 'PowerShellForGitHub' + } + + $AccessToken = Get-AccessToken -AccessToken $AccessToken + if (-not [String]::IsNullOrEmpty($AccessToken)) + { + $headers['Authorization'] = "token $AccessToken" + } + + Write-Log -Message $Description -Level Verbose + Write-Log -Message "Accessing [$Method] $url [Timeout = $(Get-GitHubConfiguration -Name WebRequestTimeoutSec))]" -Level Verbose + + $bodyAsBytes = [System.Text.Encoding]::UTF8.GetBytes($Body) + + $params = @{ + Uri = $url + Method = 'Post' + Headers = $headers + Body = $bodyAsBytes + UseDefaultCredentials = $true + UseBasicParsing = $true + TimeoutSec = Get-GitHubConfiguration -Name WebRequestTimeoutSec + } + + if (Get-GitHubConfiguration -Name LogRequestBody) + { + Write-Log -Message $Body -Level Verbose + } + + # Disable Progress Bar in function scope during Invoke-WebRequest + $ProgressPreference = 'SilentlyContinue' + + # Save Current Security Protocol + $originalSecurityProtocol = [Net.ServicePointManager]::SecurityProtocol + + # Enforce TLS v1.2 Security Protocol + [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12 + + try { + $result = Invoke-WebRequest @params + } + catch + { + Write-Debug -Message "Processing Exception $($_.Exception.PSTypeNames[0])" + + if ($_.Exception.PSTypeNames[0] -eq 'System.Net.WebException' -or + $_.Exception.PSTypeNames[0] -eq 'Microsoft.PowerShell.Commands.HttpResponseException') + { + $ex = $_.Exception + $message = $ex.Message + + if ($ex.Exception.Response -is [System.Net.WebResponse]) + { + $statusCode = $ex.Response.StatusCode.value__ # Note that value__ is not a typo. + + if ($ex.Response.PSTypeNames[0] -eq 'System.Net.Http.HttpResponseMessage') + { + $statusDescription = $ex.Response.ReasonPhrase + } + elseif ($ex.Response.PSTypeNames[0] -eq 'System.Net.HttpWebResponse') + { + $statusDescription = $ex.Response.StatusDescription + } + else { + $statusDescription = '' + } + + if ($ex.Response.Headers.Count -gt 0) + { + $requestId = $ex.Response.Headers['X-GitHub-Request-Id'] + } + } + + $innerMessage = $_.ErrorDetails.Message + try + { +# $rawContent = Get-HttpWebResponseContent -WebResponse $ex.Response + } + catch + { + Write-Log -Message "Unable to retrieve the raw HTTP Web Response:" -Exception $_ -Level Warning + } + } + else + { + Write-Log -Exception $_ -Level Error + Set-TelemetryException -Exception $_.Exception -ErrorBucket $errorBucket -Properties $localTelemetryProperties + + $exception = [Exception]::new($_.ErrorDetails.Message) + $errorRecord = [System.Management.Automation.ErrorRecord]::new( + $exception, + $statusCode, + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $_.TargetObject + ) + + if ($PSCmdlet.CommandOrigin -eq [System.Management.Automation.CommandOrigin]::Runspace) + { + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + else + { + return $errorRecord + } + } + + $output = @() + $output += $message + + if (-not [string]::IsNullOrEmpty($statusCode)) + { + $output += "$statusCode | $($statusDescription.Trim())" + } + + if (-not [string]::IsNullOrEmpty($innerMessage)) + { + try + { + $innerMessageJson = ($innerMessage | ConvertFrom-Json) + } + catch [System.ArgumentException] + { + # Will be thrown if $innerMessage isn't JSON content + $innerMessageJson = $innerMessage.Trim() + } + + if ($innerMessageJson -is [String]) + { + $output += $innerMessageJson.Trim() + } + elseif (-not [String]::IsNullOrWhiteSpace($innerMessageJson.message)) + { + $output += "$($innerMessageJson.message.Trim()) | $($innerMessageJson.documentation_url.Trim())" + if ($innerMessageJson.details) + { + $output += "$($innerMessageJson.details | Format-Table | Out-String)" + } + } + else + { + # In this case, it's probably not a normal message from the API + $output += ($innerMessageJson | Out-String) + } + } + + # It's possible that the API returned JSON content in its error response. + if (-not [String]::IsNullOrWhiteSpace($rawContent)) + { + $output += $rawContent + } + + if (-not [String]::IsNullOrEmpty($requestId)) + { + $localTelemetryProperties['RequestId'] = $requestId + $message = 'RequestId: ' + $requestId + $output += $message + Write-Log -Message $message -Level Verbose + } + + $newLineOutput = ($output -join [Environment]::NewLine) + Write-Log -Message $newLineOutput -Level Error + Set-TelemetryException -Exception $ex -ErrorBucket $errorBucket -Properties $localTelemetryProperties + $exception = [Exception]::new($newLineOutput) + $errorRecord = [System.Management.Automation.ErrorRecord]::new( + $exception, + $statusCode, + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $body + ) + + if ($PSCmdlet.CommandOrigin -eq [System.Management.Automation.CommandOrigin]::Runspace) + { + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + else + { + return $errorRecord + } + } + finally { + # Restore original security protocol + [Net.ServicePointManager]::SecurityProtocol = $originalSecurityProtocol + + # Record the telemetry for this event. + $stopwatch.Stop() + if (-not [String]::IsNullOrEmpty($TelemetryEventName)) + { + $telemetryMetrics = @{ 'Duration' = $stopwatch.Elapsed.TotalSeconds } + Set-TelemetryEvent -EventName $TelemetryEventName -Properties $localTelemetryProperties -Metrics $telemetryMetrics + } + } + + $graphQlResult = $result.Content | ConvertFrom-Json + if ($graphQlResult.errors) + { + $exception = [Exception]::new($graphQlResult.errors.message) + $errorRecord = [System.Management.Automation.ErrorRecord]::new( + $exception, + $graphQlResult.errors.type, + [System.Management.Automation.ErrorCategory]::InvalidOperation, + '' # TargetObject + ) + + if ($PSCmdlet.CommandOrigin -eq [System.Management.Automation.CommandOrigin]::Runspace) + { + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + else + { + return $errorRecord + } + } + else { + return $graphQlResult + } +} + filter Split-GitHubUri { <# diff --git a/PowerShellForGitHub.psd1 b/PowerShellForGitHub.psd1 index be840033..d72727a4 100644 --- a/PowerShellForGitHub.psd1 +++ b/PowerShellForGitHub.psd1 @@ -103,6 +103,7 @@ 'Get-GitHubRepository', 'Get-GitHubRepositoryActionsPermission', 'Get-GitHubRepositoryBranch', + 'Get-GitHubQlRepositoryBranchProtectionRule', 'Get-GitHubRepositoryBranchProtectionRule', 'Get-GitHubRepositoryCollaborator', 'Get-GitHubRepositoryContributor', @@ -121,6 +122,7 @@ 'Group-GitHubPullRequest', 'Initialize-GitHubLabel', 'Invoke-GHRestMethod', + 'Invoke-GHGraphQl', 'Invoke-GHRestMethodMultipleResult', 'Join-GitHubUri', 'Lock-GitHubIssue', @@ -143,6 +145,7 @@ 'New-GitHubRepositoryFromTemplate', 'New-GitHubRepositoryBranch', 'New-GitHubRepositoryBranchProtectionRule', + 'New-GitHubQLRepositoryBranchProtectionRule', 'New-GitHubRepositoryFork', 'New-GitHubTeam', 'Remove-GitHubAssignee', @@ -163,6 +166,7 @@ 'Remove-GitHubReleaseAsset', 'Remove-GitHubRepository', 'Remove-GitHubRepositoryBranch' + 'Remove-GitHubQlRepositoryBranchProtectionRule', 'Remove-GitHubRepositoryBranchProtectionRule', 'Remove-GitHubRepositoryTeamPermission', 'Remove-GitHubTeam', From 908c416473c9357f1a4ba8778969f1ffc8c2ea98 Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Sun, 27 Dec 2020 11:37:10 +0000 Subject: [PATCH 02/33] Updates --- Formatters/GitHubBranches.Format.ps1xml | 74 +++ GitHubBranches.ps1 | 632 ++++++++++++++---------- GitHubCore.ps1 | 53 +- Helpers.ps1 | 54 ++ PowerShellForGitHub.psd1 | 8 +- 5 files changed, 524 insertions(+), 297 deletions(-) create mode 100644 Formatters/GitHubBranches.Format.ps1xml diff --git a/Formatters/GitHubBranches.Format.ps1xml b/Formatters/GitHubBranches.Format.ps1xml new file mode 100644 index 00000000..509427b9 --- /dev/null +++ b/Formatters/GitHubBranches.Format.ps1xml @@ -0,0 +1,74 @@ + + + + + + GitHub.BranchPatternProtectionRule + + GitHub.BranchPatternProtectionRule + + + + + + + RepositoryUrl + + + pattern + + + requiredApprovingReviewCount + + + dismissesStaleReviews + + + requiresCodeOwnerReviews + + + DismissalTeams + + + DismissalUsers + + + requiresStatusChecks + + + requiresStrictStatusChecks + + + requiredStatusCheckContexts + + + requiresCommitSignatures + + + requiresLinearHistory + + + isAdminEnforced + + + RestrictPushApps + + + RestrictPushTeams + + + RestrictPushUsers + + + allowsForcePushes + + + allowsDeletions + + + + + + + + diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 index d786f8a0..7a22cef4 100644 --- a/GitHubBranches.ps1 +++ b/GitHubBranches.ps1 @@ -4,6 +4,7 @@ @{ GitHubBranchTypeName = 'GitHub.Branch' GitHubBranchProtectionRuleTypeName = 'GitHub.BranchProtectionRule' + GitHubBranchPatternProtectionRuleTypeName = 'GitHub.BranchPatternProtectionRule' }.GetEnumerator() | ForEach-Object { Set-Variable -Scope Script -Option ReadOnly -Name $_.Key -Value $_.Value } @@ -485,6 +486,7 @@ filter Remove-GitHubRepositoryBranch Invoke-GHRestMethod @params | Out-Null } + filter Get-GitHubRepositoryBranchProtectionRule { <# @@ -597,162 +599,6 @@ filter Get-GitHubRepositoryBranchProtectionRule return (Invoke-GHRestMethod @params | Add-GitHubBranchProtectionRuleAdditionalProperties) } -filter Get-GitHubQlRepositoryBranchProtectionRule -{ - <# - .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 BranchNamePattern - Name of the specific branch Pattern to be retrieved. If not supplied, all rules 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. - - .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-GitHubQlRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchNamePattern master - - Retrieves branch protection rules for the master branch of the PowerShellForGithub repository. - - .EXAMPLE - Get-GitHubQlRepositoryBranchProtectionRule -Uri 'https://github.com/microsoft/PowerShellForGitHub' -BranchNamePattern 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="The Uri parameter is only referenced by Resolve-RepositoryElements 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)] - [Alias('BranchName')] - [string] $BranchNamePattern, - - [string] $AccessToken - ) - - Write-InvocationLog - - $elements = Resolve-RepositoryElements - $OwnerName = $elements.ownerName - $RepositoryName = $elements.repositoryName - - $telemetryProperties = @{ - 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) - 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) - } - - $branchProtectionRuleFields = 'allowsDeletions allowsForcePushes dismissesStaleReviews id ' + - 'isAdminEnforced pattern requiredApprovingReviewCount requiredStatusCheckContexts ' + - 'requiresApprovingReviews requiresCodeOwnerReviews requiresCommitSignatures requiresLinearHistory ' + - 'requiresStatusChecks requiresStrictStatusChecks restrictsPushes restrictsReviewDismissals ' + - 'repository { url }' - - $hashbody = @{query = "query branchProtectionRule { repository(name: ""$RepositoryName"", " + - "owner: ""$OwnerName"") { branchProtectionRules(first: 100) { nodes { " + - " $branchProtectionRuleFields } } } }"} - - $description = "Querying $RepositoryName repository for branch protection rules" - - Write-Debug -Message $description - Write-Debug -Message "Query: $($hashbody.query)" - - $params = @{ - Body = ConvertTo-Json -InputObject $hashBody - Description = $description - AccessToken = $AccessToken - TelemetryEventName = $MyInvocation.MyCommand.Name - TelemetryProperties = $telemetryProperties - } - - $result = Invoke-GHGraphQl @params - - if ($result -is [System.Management.Automation.ErrorRecord]) - { - $PSCmdlet.ThrowTerminatingError($result) - } - - if ($result.data.repository.branchProtectionRules) - { - $rule = ($result.data.repository.branchProtectionRules.nodes | - Where-Object -Property pattern -eq $BranchNamePattern) - } - - if (!$rule) - { - $exception = [Exception]::new( - "Branch Protection Rule '$BranchNamePattern' not found on repository $RepositoryName") - $errorRecord = [System.Management.Automation.ErrorRecord]::new( - $exception, - 'BranchProtectionRuleNotFound', - [System.Management.Automation.ErrorCategory]::ObjectNotFound, - $BranchNamePattern - ) - - $PSCmdlet.ThrowTerminatingError($errorRecord) - } - - #return ($rule | Add-GitHubBranchProtectionRuleAdditionalProperties) - return $rule - -} - filter New-GitHubRepositoryBranchProtectionRule { <# @@ -1106,7 +952,129 @@ filter New-GitHubRepositoryBranchProtectionRule return (Invoke-GHRestMethod @params | Add-GitHubBranchProtectionRuleAdditionalProperties) } -filter New-GitHubQLRepositoryBranchProtectionRule +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. + + .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="The Uri parameter is only referenced by Resolve-RepositoryElements 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 + ) + + 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 (-not $PSCmdlet.ShouldProcess("'$BranchName' branch of repository '$RepositoryName'", + 'Remove GitHub Repository Branch Protection Rule')) + { + return + } + + $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 + } + + return Invoke-GHRestMethod @params | Out-Null +} + +filter New-GitHubRepositoryBranchPatternProtectionRule { <# .SYNOPSIS @@ -1133,25 +1101,25 @@ filter New-GitHubQLRepositoryBranchProtectionRule .PARAMETER OrganizationName Name of the Organization. - .PARAMETER BranchNamePattern + .PARAMETER BranchPatternName The branch name pattern to create the protection rule on. .PARAMETER StatusChecks The list of status checks to require in order to merge into the branch. - .PARAMETER RequireUpToDateBranches + .PARAMETER RequireStrictStatusChecks 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 + .PARAMETER IsAdminEnforced 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. + Specify the user names of users who can dismiss pull request reviews. .PARAMETER DismissalTeams - Specify which teams can dismiss pull request reviews. + Specify which teams can dismiss pull request reviews. This can only be + specified for organization-owned repositories. .PARAMETER DismissStaleReviews If specified, approving reviews when someone pushes a new commit are automatically @@ -1178,7 +1146,8 @@ filter New-GitHubQLRepositoryBranchProtectionRule branch. Your repository must allow squash merging or rebase merging before you can enable a linear commit history. - .PARAMETER RequireSignedCommits + .PARAMETER RequireCommitSignatures + Specifies whether commits are required to be signed. .PARAMETER AllowForcePushes Permits force pushes to the protected branch by anyone with write access to the repository. @@ -1195,19 +1164,19 @@ filter New-GitHubQLRepositoryBranchProtectionRule GitHub.Branch .OUTPUTS - GitHub.BranchRepositoryRule + GitHub.BranchPatternProtectionRule .NOTES Protecting a branch requires admin or owner permissions to the repository. .EXAMPLE - New-GitHubRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchName master -EnforceAdmins + New-GitHubRepositoryBranchPatternProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchName release/**/* -EnforceAdmins - Creates a branch protection rule for the master branch of the PowerShellForGithub repository + Creates a branch protection rule for the 'release/**/*' branch pattern of the PowerShellForGithub repository enforcing all configuration restrictions for administrators. .EXAMPLE - New-GitHubRepositoryBranchProtectionRule -Uri 'https://github.com/microsoft/PowerShellForGitHub' -BranchName master -RequiredApprovingReviewCount 1 + New-GitHubRepositoryBranchPatternProtectionRule -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. @@ -1216,7 +1185,7 @@ filter New-GitHubQLRepositoryBranchProtectionRule PositionalBinding = $false, SupportsShouldProcess, DefaultParameterSetName = 'Elements')] - [OutputType({$script:GitHubBranchProtectionRuleTypeName })] + [OutputType({$script:GitHubBranchPatternProtectionRuleTypeName })] param( [Parameter(ParameterSetName = 'Elements')] [string] $OwnerName, @@ -1236,16 +1205,14 @@ filter New-GitHubQLRepositoryBranchProtectionRule [Parameter( Mandatory, - ValueFromPipelineByPropertyName, Position = 2)] - [Alias('BranchName')] - [string] $BranchNamePattern, + [string] $BranchPatternName, [string[]] $StatusChecks, - [switch] $RequireUpToDateBranches, + [switch] $RequireStrictStatusChecks, - [switch] $EnforceAdmins, + [switch] $IsAdminEnforced, [string[]] $DismissalUsers, @@ -1270,7 +1237,7 @@ filter New-GitHubQLRepositoryBranchProtectionRule [switch] $AllowDeletions, - [switch] $RequireSignedCommits, + [switch] $RequireCommitSignatures, [string] $AccessToken ) @@ -1314,15 +1281,15 @@ filter New-GitHubQLRepositoryBranchProtectionRule $repoId = $result.data.repository.id $mutationList = @( - "repositoryId: ""$repoId"", pattern: ""$BranchNamePattern""" + "repositoryId: ""$repoId"", pattern: ""$BranchPatternName""" 'requiresLinearHistory: ' + $RequireLinearHistory.ToBool().ToString().ToLower() 'allowsForcePushes: ' + $AllowForcePushes.ToBool().ToString().ToLower() 'allowsDeletions: ' + $AllowDeletions.ToBool().ToString().ToLower() - 'isAdminEnforced: ' + $EnforceAdmins.ToBool().ToString().ToLower() + 'isAdminEnforced: ' + $IsAdminEnforced.ToBool().ToString().ToLower() 'dismissesStaleReviews: ' + $DismissStaleReviews.ToBool().ToString().ToLower() 'requiresCodeOwnerReviews: ' + $RequireCodeOwnerReviews.ToBool().ToString().ToLower() - 'requiresStrictStatusChecks: ' + $RequireUpToDateBranches.ToBool().ToString().ToLower() - 'requiresCommitSignatures: ' + $RequireSignedCommits.ToBool().ToString().ToLower() + 'requiresStrictStatusChecks: ' + $RequireStrictStatusChecks.ToBool().ToString().ToLower() + 'requiresCommitSignatures: ' + $RequireCommitSignatures.ToBool().ToString().ToLower() ) if ($PSBoundParameters.ContainsKey('RequiredApprovingReviewCount')) @@ -1401,13 +1368,13 @@ filter New-GitHubQLRepositoryBranchProtectionRule } else { - $exception = [Exception]::new("Team $team not found in organization $OrganizationName") - $errorRecord = [System.Management.Automation.ErrorRecord]::new( - $exception, - 'RestictPushTeamNotFound', - [System.Management.Automation.ErrorCategory]::ObjectNotFound, - $team - ) + $newErrorRecordParms = @{ + ErrorMessage = "Team $team not found with write permissions to $RepositoryName" + ErrorId = 'RestictPushTeamNotFound' + ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound + TargetObject = $team + } + $errorRecord = New-ErrorRecord @newErrorRecordParms $PSCmdlet.ThrowTerminatingError($errorRecord) } @@ -1445,13 +1412,13 @@ filter New-GitHubQLRepositoryBranchProtectionRule } else { - $exception = [Exception]::new("App $app not found") - $errorRecord = [System.Management.Automation.ErrorRecord]::new( - $exception, - 'RestictPushAppNotFound', - [System.Management.Automation.ErrorCategory]::ObjectNotFound, - $team - ) + $newErrorRecordParms = @{ + ErrorMessage = "App $app not found with write permissions to $RepositoryName" + ErrorId = 'RestictPushAppNotFound' + ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound + TargetObject = $app + } + $errorRecord = New-ErrorRecord @newErrorRecordParms $PSCmdlet.ThrowTerminatingError($errorRecord) } @@ -1528,13 +1495,13 @@ filter New-GitHubQLRepositoryBranchProtectionRule } else { - $exception = [Exception]::new("Team $team not found in organization $OrganizationName") - $errorRecord = [System.Management.Automation.ErrorRecord]::new( - $exception, - 'DismissalTeamNotFound', - [System.Management.Automation.ErrorCategory]::ObjectNotFound, - $team - ) + $newErrorRecordParms = @{ + ErrorMessage = "Team $team not found in organization $OrganizationName" + ErrorId = 'DismissalTeamNotFound' + ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound + TargetObject = $team + } + $errorRecord = New-ErrorRecord @newErrorRecordParms $PSCmdlet.ThrowTerminatingError($errorRecord) } @@ -1549,7 +1516,7 @@ filter New-GitHubQLRepositoryBranchProtectionRule $hashbody = @{query = "mutation ProtectionRule { createBranchProtectionRule(input: { $mutationInput }) " + "{ clientMutationId } } " } - $description = "Setting $BranchNamePattern branch protection status for $RepositoryName" + $description = "Setting $BranchPatternName branch protection status for $RepositoryName" $body = ConvertTo-Json -InputObject $hashBody Write-Debug -Message $description @@ -1557,7 +1524,7 @@ filter New-GitHubQLRepositoryBranchProtectionRule if (-not $PSCmdlet.ShouldProcess( "Owner '$OwnerName', Repository '$RepositoryName'", - "Create '$BranchNamePattern' branch pattern GitHub Repository Branch Protection Rule")) + "Create '$BranchPatternName' branch pattern GitHub Repository Branch Protection Rule")) { return } @@ -1578,14 +1545,14 @@ filter New-GitHubQLRepositoryBranchProtectionRule } } -filter Remove-GitHubRepositoryBranchProtectionRule +filter Get-GitHubRepositoryBranchPatternProtectionRule { <# .SYNOPSIS - Remove branch protection rules from a given GitHub repository. + Retrieve a branch pattern protection rule for a given GitHub repository. .DESCRIPTION - Remove branch protection rules from a given GitHub repository. + Retrieve a branch pattern protection rule for a given GitHub repository. The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub @@ -1602,43 +1569,47 @@ filter Remove-GitHubRepositoryBranchProtectionRule 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 BranchPatternName + Name of the specific branch Pattern to 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. .INPUTS - GitHub.Repository 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 - None - - .EXAMPLE - Remove-GitHubRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchName master - - Removes branch protection rules from the master branch of the PowerShellForGithub repository. + GitHub.BranchPatternProtectionRule .EXAMPLE - Removes-GitHubRepositoryBranchProtection -Uri 'https://github.com/microsoft/PowerShellForGitHub' -BranchName master + Get-GitHubRepositoryBranchPatternProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchPatternName release/**/* - Removes branch protection rules from the master branch of the PowerShellForGithub repository. + Retrieves branch protection rules for the release/**/* branch pattern of the PowerShellForGithub repository. .EXAMPLE - Removes-GitHubRepositoryBranchProtection -Uri 'https://github.com/master/PowerShellForGitHub' -BranchName master -Force + Get-GitHubQlRepositoryBranchPatternProtectionRule -Uri 'https://github.com/microsoft/PowerShellForGitHub' -BranchPatternName master - Removes branch protection rules from the master branch of the PowerShellForGithub repository - without prompting for confirmation. + Retrieves branch protection rules for the master branch pattern of the PowerShellForGithub repository. #> [CmdletBinding( PositionalBinding = $false, - SupportsShouldProcess, - DefaultParameterSetName = 'Elements', - ConfirmImpact = "High")] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "", Justification="The Uri parameter is only referenced by Resolve-RepositoryElements which get access to it from the stack via Get-Variable -Scope 1.")] - [Alias('Delete-GitHubRepositoryBranchProtectionRule')] + DefaultParameterSetName = 'Elements')] + [OutputType({ $script:GitHubBranchProtectionRuleTypeName })] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "", + Justification="The Uri parameter is only referenced by Resolve-RepositoryElements which get access to it from the stack via Get-Variable -Scope 1.")] param( [Parameter(ParameterSetName = 'Elements')] [string] $OwnerName, @@ -1656,11 +1627,8 @@ filter Remove-GitHubRepositoryBranchProtectionRule [Parameter( Mandatory, - ValueFromPipelineByPropertyName, Position = 2)] - [string] $BranchName, - - [switch] $Force, + [string] $BranchPatternName, [string] $AccessToken ) @@ -1676,38 +1644,70 @@ filter Remove-GitHubRepositoryBranchProtectionRule 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) } - if ($Force -and (-not $Confirm)) - { - $ConfirmPreference = 'None' - } + $branchProtectionRuleFields = 'allowsDeletions allowsForcePushes dismissesStaleReviews id ' + + 'isAdminEnforced pattern requiredApprovingReviewCount requiredStatusCheckContexts ' + + 'requiresApprovingReviews requiresCodeOwnerReviews requiresCommitSignatures requiresLinearHistory ' + + 'requiresStatusChecks requiresStrictStatusChecks restrictsPushes restrictsReviewDismissals ' + + 'pushAllowances(first: 100) { nodes { actor { ... on App { __typename name } ' + + '... on Team { __typename name } ... on User { __typename login } } } }' + + 'reviewDismissalAllowances(first: 100) { nodes { actor { ... on Team { __typename name } ' + + '... on User { __typename login } } } } ' + + 'repository { url }' - if (-not $PSCmdlet.ShouldProcess("'$BranchName' branch of repository '$RepositoryName'", - 'Remove GitHub Repository Branch Protection Rule')) - { - return - } + $hashbody = @{query = "query branchProtectionRule { repository(name: ""$RepositoryName"", " + + "owner: ""$OwnerName"") { branchProtectionRules(first: 100) { nodes { " + + "$branchProtectionRuleFields } } } }"} + + $description = "Querying $RepositoryName repository for branch protection rules" + + Write-Debug -Message $description + Write-Debug -Message "Query: $($hashbody.query)" $params = @{ - UriFragment = "repos/$OwnerName/$RepositoryName/branches/$BranchName/protection" - Description = "Removing $BranchName branch protection rule for $RepositoryName" - Method = 'Delete' - AcceptHeader = $script:lukeCageAcceptHeader + Body = ConvertTo-Json -InputObject $hashBody + Description = $description AccessToken = $AccessToken TelemetryEventName = $MyInvocation.MyCommand.Name TelemetryProperties = $telemetryProperties } - return Invoke-GHRestMethod @params | Out-Null + $result = Invoke-GHGraphQl @params + + if ($result -is [System.Management.Automation.ErrorRecord]) + { + $PSCmdlet.ThrowTerminatingError($result) + } + + if ($result.data.repository.branchProtectionRules) + { + $rule = ($result.data.repository.branchProtectionRules.nodes | + Where-Object -Property pattern -eq $BranchPatternName) + } + + if (!$rule) + { + $newErrorRecordParms = @{ + ErrorMessage = "Branch Protection Rule '$BranchPatternName' not found on repository $RepositoryName" + ErrorId = 'BranchProtectionRuleNotFound' + ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound + TargetObject = $BranchPatternName + } + $errorRecord = New-ErrorRecord @newErrorRecordParms + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + return ($rule | Add-GitHubBranchPatternProtectionRuleAdditionalProperties) } -filter Remove-GitHubQlRepositoryBranchProtectionRule +filter Remove-GitHubRepositoryBranchPatternProtectionRule { <# .SYNOPSIS - Remove branch protection rules from a given GitHub repository. + Remove a branch pattern protection rule from a given GitHub repository. .DESCRIPTION - Remove branch protection rules from a given GitHub repository. + Remove a branch pattern protection rule from a given GitHub repository. The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub @@ -1724,8 +1724,8 @@ filter Remove-GitHubQlRepositoryBranchProtectionRule The OwnerName and RepositoryName will be extracted from here instead of needing to provide them individually. - .PARAMETER BranchNamePattern - Name of the specific branch pattern to remove the branch protection rule from. + .PARAMETER BranchPatternName + Name of the specific branch protection rule pattern to remove. .PARAMETER AccessToken If provided, this will be used as the AccessToken for authentication with the @@ -1733,25 +1733,24 @@ filter Remove-GitHubQlRepositoryBranchProtectionRule .INPUTS GitHub.Repository - GitHub.Branch .OUTPUTS None .EXAMPLE - Remove-GitHubQLRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchNamePattern master + Remove-GitHubRepositoryBranchPatternProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchPatternName release/**/* - Removes branch protection rules from the master branch of the PowerShellForGithub repository. + Removes branch pattern 'release/**/*' protection rules from the PowerShellForGithub repository. .EXAMPLE - Removes-GitHubQLRepositoryBranchProtection -Uri 'https://github.com/microsoft/PowerShellForGitHub' -BranchNamePattern master + Remove-GitHubRepositoryBranchPatternProtectionRule -Uri 'https://github.com/microsoft/PowerShellForGitHub' -BranchPatternName release/**/* - Removes branch protection rules from the master branch of the PowerShellForGithub repository. + Removes branch pattern 'release/**/*' protection rules from the PowerShellForGithub repository. .EXAMPLE - Removes-GitHubQLRepositoryBranchProtection -Uri 'https://github.com/master/PowerShellForGitHub' -BranchNamePattern master -Force + Remove-GitHubRepositoryBranchPatternProtectionRule -Uri 'https://github.com/master/PowerShellForGitHub' -BranchPatternName release/**/* -Force - Removes branch protection rules from the master branch of the PowerShellForGithub repository + Removes branch pattern 'release/**/*' protection rules from the PowerShellForGithub repository without prompting for confirmation. #> [CmdletBinding( @@ -1761,7 +1760,7 @@ filter Remove-GitHubQlRepositoryBranchProtectionRule ConfirmImpact = "High")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "", Justification="The Uri parameter is only referenced by Resolve-RepositoryElements which get access to it from the stack via Get-Variable -Scope 1.")] - [Alias('Delete-GitHubQLRepositoryBranchProtectionRule')] + [Alias('Delete-GitHubRepositoryBranchPatternProtectionRule')] param( [Parameter(ParameterSetName = 'Elements')] [string] $OwnerName, @@ -1779,10 +1778,8 @@ filter Remove-GitHubQlRepositoryBranchProtectionRule [Parameter( Mandatory, - ValueFromPipelineByPropertyName, Position = 2)] - [Alias('BranchName')] - [string] $BranchNamePattern, + [string] $BranchPatternName, [switch] $Force, @@ -1825,19 +1822,18 @@ filter Remove-GitHubQlRepositoryBranchProtectionRule if ($result.data.repository.branchProtectionRules) { $ruleId = ($result.data.repository.branchProtectionRules.nodes | - Where-Object -Property pattern -eq $BranchNamePattern).id + Where-Object -Property pattern -eq $BranchPatternName).id } if (!$ruleId) { - $exception = [Exception]::new( - "Branch Protection Rule '$BranchNamePattern' not found on repository $RepositoryName") - $errorRecord = [System.Management.Automation.ErrorRecord]::new( - $exception, - 'BranchProtectionRuleNotFound', - [System.Management.Automation.ErrorCategory]::ObjectNotFound, - $BranchNamePattern - ) + $newErrorRecordParms = @{ + ErrorMessage = "Branch Protection Rule '$BranchPatternName' not found on repository $RepositoryName" + ErrorId = 'BranchProtectionRuleNotFound' + ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound + TargetObject = $BranchPatternName + } + $errorRecord = New-ErrorRecord @newErrorRecordParms $PSCmdlet.ThrowTerminatingError($errorRecord) } @@ -1850,13 +1846,13 @@ filter Remove-GitHubQlRepositoryBranchProtectionRule $hashbody = @{query = "mutation ProtectionRule { deleteBranchProtectionRule(input: " + "{ branchProtectionRuleId: ""$ruleId"" } ) { clientMutationId } }" } - $description = "Removing $BranchNamePattern branch protection rule for $RepositoryName" + $description = "Removing $BranchPatternName branch protection rule for $RepositoryName" $body = ConvertTo-Json -InputObject $hashBody Write-Debug -Message $description Write-Debug -Message "Query: $body" - if (-not $PSCmdlet.ShouldProcess("'$BranchNamePattern' branch of repository '$RepositoryName'", + if (-not $PSCmdlet.ShouldProcess("'$BranchPatternName' branch of repository '$RepositoryName'", 'Remove GitHub Repository Branch Protection Rule')) { return @@ -2004,3 +2000,101 @@ filter Add-GitHubBranchProtectionRuleAdditionalProperties Write-Output $item } } + +filter Add-GitHubBranchPatternProtectionRuleAdditionalProperties +{ + <# + .SYNOPSIS + Adds type name and additional properties to ease pipelining to GitHub Branch Pattern 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.BranchPatternProtection Rule +#> + [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:GitHubBranchPatternProtectionRuleTypeName + ) + + foreach ($item in $InputObject) + { + $item.PSObject.TypeNames.Insert(0, $TypeName) + + if (-not (Get-GitHubConfiguration -Name DisablePipelineSupport)) + { + $elements = Split-GitHubUri -Uri $item.repository.url + $repositoryUrl = Join-GitHubUri @elements + Add-Member -InputObject $item -Name 'RepositoryUrl' -Value $repositoryUrl -MemberType NoteProperty -Force + } + + $restrictPushApps = @() + $restrictPushTeams = @() + $RestrictPushUsers = @() + + foreach ($actor in $item.pushAllowances.nodes.actor) + { + if ($actor.__typename -eq 'App') + { + $restrictPushApps += $actor.name + } + elseif ($actor.__typename -eq 'Team') + { + $restrictPushTeams += $actor.name + } + elseif ($actor.__typename -eq 'User') + { + $RestrictPushUsers += $actor.login + } + else + { + Write-Warning "Unknown restrict push actor type found $($actor.__typename). Ignoring" + } + } + + Add-Member -InputObject $item -Name 'RestrictPushApps' -Value $restrictPushApps -MemberType NoteProperty -Force + Add-Member -InputObject $item -Name 'RestrictPushTeams' -Value $restrictPushTeams -MemberType NoteProperty -Force + Add-Member -InputObject $item -Name 'RestrictPushUsers' -Value $restrictPushUsers -MemberType NoteProperty -Force + + $dismissalTeams = @() + $dismissalUsers = @() + + foreach ($actor in $item.reviewDismissalAllowances.nodes.actor) + { + if ($actor.__typename -eq 'Team') + { + $dismissalTeams += $actor.name + } + elseif ($actor.__typename -eq 'User') + { + $dismissalUsers += $actor.login + } + else + { + Write-Warning "Unknown dismissal actor type found $($actor.__typename). Ignoring" + } + } + + Add-Member -InputObject $item -Name 'DismissalTeams' -Value $dismissalTeams -MemberType NoteProperty -Force + Add-Member -InputObject $item -Name 'DismissalUsers' -Value $dismissalUsers -MemberType NoteProperty -Force + + Write-Output $item + } +} diff --git a/GitHubCore.ps1 b/GitHubCore.ps1 index b3de8900..16bb8b03 100644 --- a/GitHubCore.ps1 +++ b/GitHubCore.ps1 @@ -882,6 +882,8 @@ function Invoke-GHGraphQl { Write-Debug -Message "Processing Exception $($_.Exception.PSTypeNames[0])" + # PowerShell 5 Invoke-WebRequest returns a 'System.Net.WebException' object on error + # PowerShell 6+ Invoke-WebRequest returns a 'Microsoft.PowerShell.Commands.HttpResponseException' object on error if ($_.Exception.PSTypeNames[0] -eq 'System.Net.WebException' -or $_.Exception.PSTypeNames[0] -eq 'Microsoft.PowerShell.Commands.HttpResponseException') { @@ -890,7 +892,7 @@ function Invoke-GHGraphQl if ($ex.Exception.Response -is [System.Net.WebResponse]) { - $statusCode = $ex.Response.StatusCode.value__ # Note that value__ is not a typo. + $statusCode = $ex.Response.StatusCode.value__ if ($ex.Response.PSTypeNames[0] -eq 'System.Net.Http.HttpResponseMessage') { @@ -922,16 +924,15 @@ function Invoke-GHGraphQl } else { - Write-Log -Exception $_ -Level Error Set-TelemetryException -Exception $_.Exception -ErrorBucket $errorBucket -Properties $localTelemetryProperties - $exception = [Exception]::new($_.ErrorDetails.Message) - $errorRecord = [System.Management.Automation.ErrorRecord]::new( - $exception, - $statusCode, - [System.Management.Automation.ErrorCategory]::InvalidOperation, - $_.TargetObject - ) + $newErrorRecordParms = @{ + ErrorMessage = $_.ErrorDetails.Message + ErrorId = $statusCode + ErrorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation + TargetObject = $body + } + $errorRecord = New-ErrorRecord @newErrorRecordParms if ($PSCmdlet.CommandOrigin -eq [System.Management.Automation.CommandOrigin]::Runspace) { @@ -997,15 +998,15 @@ function Invoke-GHGraphQl } $newLineOutput = ($output -join [Environment]::NewLine) - Write-Log -Message $newLineOutput -Level Error Set-TelemetryException -Exception $ex -ErrorBucket $errorBucket -Properties $localTelemetryProperties - $exception = [Exception]::new($newLineOutput) - $errorRecord = [System.Management.Automation.ErrorRecord]::new( - $exception, - $statusCode, - [System.Management.Automation.ErrorCategory]::InvalidOperation, - $body - ) + + $newErrorRecordParms = @{ + ErrorMessage = $newLineOutput + ErrorId = $statusCode + ErrorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation + TargetObject = $body + } + $errorRecord = New-ErrorRecord @newErrorRecordParms if ($PSCmdlet.CommandOrigin -eq [System.Management.Automation.CommandOrigin]::Runspace) { @@ -1030,15 +1031,16 @@ function Invoke-GHGraphQl } $graphQlResult = $result.Content | ConvertFrom-Json + if ($graphQlResult.errors) { - $exception = [Exception]::new($graphQlResult.errors.message) - $errorRecord = [System.Management.Automation.ErrorRecord]::new( - $exception, - $graphQlResult.errors.type, - [System.Management.Automation.ErrorCategory]::InvalidOperation, - '' # TargetObject - ) + write-debug ($graphqlResult.errors|Fl|out-string) + $newErrorRecordParms = @{ + ErrorMessage = $graphQlResult.errors[0].message + ErrorId = $graphQlResult.errors[0].type + ErrorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation + } + $errorRecord = New-ErrorRecord @newErrorRecordParms if ($PSCmdlet.CommandOrigin -eq [System.Management.Automation.CommandOrigin]::Runspace) { @@ -1049,7 +1051,8 @@ function Invoke-GHGraphQl return $errorRecord } } - else { + else + { return $graphQlResult } } diff --git a/Helpers.ps1 b/Helpers.ps1 index 4e9116da..afca8357 100644 --- a/Helpers.ps1 +++ b/Helpers.ps1 @@ -663,3 +663,57 @@ function Get-HttpWebResponseContent } } } + +function New-ErrorRecord +{ +<# + .SYNOPSIS + Returns an ErrorRecord object for use by $PSCmdlet.ThrowTerminatingError + + .DESCRIPTION + Returns an ErrorRecord object for use by $PSCmdlet.ThrowTerminatingError + + .PARAMETER ErrorMessage + The message that describes the error + + .PARAMETER ErrorId + The Id to be used to construct the FullyQualifiedErrorId property of the error record. + + .PARAMETER ErrorCategory + This is the ErrorCategory which best describes the error. + + .PARAMETER TargetObject + This is the object against which the cmdlet was operating when the error occurred. This is optional. + + .OUTPUTS + System.Management.Automation.ErrorRecord + + .NOTES + ErrorRecord Class - https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.errorrecord + Exception Class - https://docs.microsoft.com/en-us/dotnet/api/system.exception + Cmdlet.ThrowTerminationError - https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.cmdlet.throwterminatingerror +#> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', + Justification = 'This function is non state changing.')] + [OutputType([System.Management.Automation.ErrorRecord])] + param( + [Parameter(Mandatory)] + [System.String] $ErrorMessage, + + [System.String] $ErrorId, + + [Parameter(Mandatory)] + [System.Management.Automation.ErrorCategory] $ErrorCategory, + + [System.Management.Automation.PSObject] $TargetObject + ) + + Write-Log -Message $ErrorMessage -Level Error + + $exception = New-Object -TypeName System.Exception -ArgumentList $ErrorMessage + $errorRecordArgumentList = $exception, $ErrorId, $ErrorCategory, $TargetObject + $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $errorRecordArgumentList + + return $errorRecord +} diff --git a/PowerShellForGitHub.psd1 b/PowerShellForGitHub.psd1 index d72727a4..db633e70 100644 --- a/PowerShellForGitHub.psd1 +++ b/PowerShellForGitHub.psd1 @@ -15,6 +15,7 @@ # Format files (.ps1xml) to be loaded when importing this module FormatsToProcess = @( + 'Formatters/GitHubBranches.Format.ps1xml', 'Formatters/GitHubGistComments.Format.ps1xml', 'Formatters/GitHubGists.Format.ps1xml', 'Formatters/GitHubReleases.Format.ps1xml' @@ -103,7 +104,7 @@ 'Get-GitHubRepository', 'Get-GitHubRepositoryActionsPermission', 'Get-GitHubRepositoryBranch', - 'Get-GitHubQlRepositoryBranchProtectionRule', + 'Get-GitHubRepositoryBranchPatternProtectionRule', 'Get-GitHubRepositoryBranchProtectionRule', 'Get-GitHubRepositoryCollaborator', 'Get-GitHubRepositoryContributor', @@ -144,8 +145,8 @@ 'New-GitHubRepository', 'New-GitHubRepositoryFromTemplate', 'New-GitHubRepositoryBranch', + 'New-GitHubRepositoryBranchPatternProtectionRule', 'New-GitHubRepositoryBranchProtectionRule', - 'New-GitHubQLRepositoryBranchProtectionRule', 'New-GitHubRepositoryFork', 'New-GitHubTeam', 'Remove-GitHubAssignee', @@ -166,7 +167,7 @@ 'Remove-GitHubReleaseAsset', 'Remove-GitHubRepository', 'Remove-GitHubRepositoryBranch' - 'Remove-GitHubQlRepositoryBranchProtectionRule', + 'Remove-GitHubRepositoryBranchPatternProtectionRule', 'Remove-GitHubRepositoryBranchProtectionRule', 'Remove-GitHubRepositoryTeamPermission', 'Remove-GitHubTeam', @@ -227,6 +228,7 @@ 'Delete-GitHubReleaseAsset', 'Delete-GitHubRepository', 'Delete-GitHubRepositoryBranch', + 'Delete-GitHubRepositoryBranchPatternProtectionRule', 'Delete-GitHubRepositoryBranchProtectionRule', 'Delete-GitHubRepositoryTeamPermission', 'Delete-GitHubTeam', From 7698f3925dea67fb5aaa8278ea2e8dc1fa67002a Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Sun, 10 Jan 2021 11:03:17 +0000 Subject: [PATCH 03/33] Update new branch pattern and usage --- GitHubBranches.ps1 | 295 ++++++++++++++++++++++++++++++--------------- USAGE.md | 21 ++++ 2 files changed, 217 insertions(+), 99 deletions(-) diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 index 7a22cef4..93296738 100644 --- a/GitHubBranches.ps1 +++ b/GitHubBranches.ps1 @@ -1282,40 +1282,186 @@ filter New-GitHubRepositoryBranchPatternProtectionRule $mutationList = @( "repositoryId: ""$repoId"", pattern: ""$BranchPatternName""" - 'requiresLinearHistory: ' + $RequireLinearHistory.ToBool().ToString().ToLower() - 'allowsForcePushes: ' + $AllowForcePushes.ToBool().ToString().ToLower() - 'allowsDeletions: ' + $AllowDeletions.ToBool().ToString().ToLower() - 'isAdminEnforced: ' + $IsAdminEnforced.ToBool().ToString().ToLower() - 'dismissesStaleReviews: ' + $DismissStaleReviews.ToBool().ToString().ToLower() - 'requiresCodeOwnerReviews: ' + $RequireCodeOwnerReviews.ToBool().ToString().ToLower() - 'requiresStrictStatusChecks: ' + $RequireStrictStatusChecks.ToBool().ToString().ToLower() - 'requiresCommitSignatures: ' + $RequireCommitSignatures.ToBool().ToString().ToLower() ) - if ($PSBoundParameters.ContainsKey('RequiredApprovingReviewCount')) + # Process 'Require pull request reviews before merging' properties + if ($PSBoundParameters.ContainsKey('RequiredApprovingReviewCount') -or + $PSBoundParameters.ContainsKey('DismissStaleReviews') -or + $PSBoundParameters.ContainsKey('RequireCodeOwnerReviews') -or + $PSBoundParameters.ContainsKey('DismissalUsers') -or + $PSBoundParameters.ContainsKey('DismissalTeams')) { $mutationList += 'requiresApprovingReviews: true' - $mutationList += 'requiredApprovingReviewCount: ' + $RequiredApprovingReviewCount + + if ($PSBoundParameters.ContainsKey('RequiredApprovingReviewCount')) + { + $mutationList += 'requiredApprovingReviewCount: ' + $RequiredApprovingReviewCount + } + + if ($PSBoundParameters.ContainsKey('DismissStaleReviews')) + { + $mutationList += 'dismissesStaleReviews: ' + $DismissStaleReviews.ToBool().ToString().ToLower() + } + + if ($PSBoundParameters.ContainsKey('RequireCodeOwnerReviews')) + { + $mutationList += 'requiresCodeOwnerReviews: ' + $RequireCodeOwnerReviews.ToBool().ToString().ToLower() + } + + if ($PSBoundParameters.ContainsKey('DismissalUsers') -or + $PSBoundParameters.ContainsKey('DismissalTeams')) + { + $reviewDismissalActorIds = @() + + If ($PSBoundParameters.ContainsKey('DismissalUsers')) + { + Foreach($user in $DismissalUsers) + { + $hashbody = @{query = "query user { user(login: ""$user"") { id } }"} + + $description = "Querying user $user" + + Write-Debug -Message $description + + $params = @{ + Body = ConvertTo-Json -InputObject $hashBody + Description = $description + AccessToken = $AccessToken + TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryProperties = $telemetryProperties + } + + $result = Invoke-GHGraphQl @params + + if ($result -is [System.Management.Automation.ErrorRecord]) + { + $PSCmdlet.ThrowTerminatingError($result) + } + + $reviewDismissalActorIds += $result.data.user.id + } + } + + If ($PSBoundParameters.ContainsKey('DismissalTeams')) + { + Foreach($team in $DismissalTeams) + { + $hashbody = @{query = "query organization { organization(login: ""$OrganizationName"") " + + "{ team(slug: ""$team"") { id } } }"} + + $description = "Querying $OrganizationName organisation for team $team" + + Write-Debug -Message $description + + $params = @{ + Body = ConvertTo-Json -InputObject $hashBody + Description = $description + AccessToken = $AccessToken + TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryProperties = $telemetryProperties + } + + $result = Invoke-GHGraphQl @params + + if ($result -is [System.Management.Automation.ErrorRecord]) + { + $PSCmdlet.ThrowTerminatingError($result) + } + + if ([System.String]::IsNullOrEmpty($result.data.organization.team)) + { + $newErrorRecordParms = @{ + ErrorMessage = "Team $team not found in organization $OrganizationName" + ErrorId = 'DismissalTeamNotFound' + ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound + TargetObject = $team + } + $errorRecord = New-ErrorRecord @newErrorRecordParms + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + $getGitHubRepositoryTeamPermissionParms = @{ + TeamName = $team + OwnerName = $ownerName + RepositoryName = $repositoryName + } + + $teamPermission = Get-GitHubRepositoryTeamPermission @getGitHubRepositoryTeamPermissionParms + + if ($teamPermission.permissions.push -eq $true -or $teamPermission.permissions.maintain -eq $true) + { + $reviewDismissalActorIds += $result.data.organization.team.id + } + else + { + $newErrorRecordParms = @{ + ErrorMessage = "Team $team does not have push or maintain permissions on repository $RepositoryName" + ErrorId = 'DismissalTeamNoPermissions' + ErrorCategory = [System.Management.Automation.ErrorCategory]::PermissionDenied + TargetObject = $team + } + + $errorRecord = New-ErrorRecord @newErrorRecordParms + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + } + } + + $mutationList += 'restrictsReviewDismissals: true' + $mutationList += 'reviewDismissalActorIds: [ "' + ($reviewDismissalActorIds -join ('","')) + '" ]' + } } - if ($PSBoundParameters.ContainsKey('StatusChecks')) + + # Process 'Require status checks to pass before merging' properties + if ($PSBoundParameters.ContainsKey('StatusChecks') -or + $PSBoundParameters.ContainsKey('RequireStrictStatusChecks')) { $mutationList += 'requiresStatusChecks: true' - $mutationList += 'requiredStatusCheckContexts: [ "' + ($StatusChecks -join ('","')) + '" ]' + + if ($PSBoundParameters.ContainsKey('RequireStrictStatusChecks')) + { + $mutationList += 'requiresStrictStatusChecks: ' + $RequireStrictStatusChecks.ToBool().ToString().ToLower() + } + + if ($PSBoundParameters.ContainsKey('StatusChecks')) + { + $mutationList += 'requiredStatusCheckContexts: [ "' + ($StatusChecks -join ('","')) + '" ]' + } + } + + if ($PSBoundParameters.ContainsKey('RequireCommitSignatures')) + { + $mutationList += 'requiresCommitSignatures: ' + $RequireCommitSignatures.ToBool().ToString().ToLower() + } + + if ($PSBoundParameters.ContainsKey('RequireLinearHistory')) + { + $mutationList += 'requiresLinearHistory: ' + $RequireLinearHistory.ToBool().ToString().ToLower() } - If ($PSBoundParameters.ContainsKey('RestrictPushUsers') -or + if ($PSBoundParameters.ContainsKey('IsAdminEnforced')) + { + $mutationList += 'isAdminEnforced: ' + $IsAdminEnforced.ToBool().ToString().ToLower() + } + + # Process 'Restrict who can push to matching branches' properties + if ($PSBoundParameters.ContainsKey('RestrictPushUsers') -or $PSBoundParameters.ContainsKey('RestrictPushTeams') -or $PSBoundParameters.ContainsKey('RestrictPushApps')) { $restrictPushActorIds = @() - If ($PSBoundParameters.ContainsKey('RestrictPushUsers')) + if ($PSBoundParameters.ContainsKey('RestrictPushUsers')) { - Foreach($user in $RestrictPushUsers) + foreach($user in $RestrictPushUsers) { $hashbody = @{query = "query user { user(login: ""$user"") { id } }"} $description = "Querying User $user" + Write-Debug -Message $description $params = @{ @@ -1357,23 +1503,47 @@ filter New-GitHubRepositoryBranchPatternProtectionRule } $result = Invoke-GHGraphQl @params + if ($result -is [System.Management.Automation.ErrorRecord]) { $PSCmdlet.ThrowTerminatingError($result) } - if ($result.data.organization.team) + if ([System.String]::IsNullOrEmpty($result.data.organization.team)) + { + $newErrorRecordParms = @{ + ErrorMessage = "Team $team not found in organization $OrganizationName" + ErrorId = 'DismissalTeamNotFound' + ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound + TargetObject = $team + } + + $errorRecord = New-ErrorRecord @newErrorRecordParms + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + $getGitHubRepositoryTeamPermissionParms = @{ + TeamName = $team + OwnerName = $ownerName + RepositoryName = $repositoryName + } + + $teamPermission = Get-GitHubRepositoryTeamPermission @getGitHubRepositoryTeamPermissionParms + + if ($teamPermission.permissions.push -eq $true -or $teamPermission.permissions.maintain -eq $true) { $restrictPushActorIds += $result.data.organization.team.id } else { $newErrorRecordParms = @{ - ErrorMessage = "Team $team not found with write permissions to $RepositoryName" - ErrorId = 'RestictPushTeamNotFound' - ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound + ErrorMessage = "Team $team does not have push or maintain permissions on repository $RepositoryName" + ErrorId = 'PushTeamNoPermissions' + ErrorCategory = [System.Management.Automation.ErrorCategory]::PermissionDenied TargetObject = $team } + $errorRecord = New-ErrorRecord @newErrorRecordParms $PSCmdlet.ThrowTerminatingError($errorRecord) @@ -1413,7 +1583,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule else { $newErrorRecordParms = @{ - ErrorMessage = "App $app not found with write permissions to $RepositoryName" + ErrorMessage = "App $app not found in marketplace" ErrorId = 'RestictPushAppNotFound' ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound TargetObject = $app @@ -1429,87 +1599,14 @@ filter New-GitHubRepositoryBranchPatternProtectionRule $mutationList += 'pushActorIds: [ "' + ($restrictPushActorIds -join ('","')) + '" ]' } - if ($PSBoundParameters.ContainsKey('DismissalUsers') -or - $PSBoundParameters.ContainsKey('DismissalTeams')) + if ($PSBoundParameters.ContainsKey('AllowForcePushes')) { - $reviewDismissalActorIds = @() - - If ($PSBoundParameters.ContainsKey('DismissalUsers')) - { - Foreach($user in $DismissalUsers) - { - $hashbody = @{query = "query user { user(login: ""$user"") { id } }"} - - $description = "Querying user $user" - - Write-Debug -Message $description - - $params = @{ - Body = ConvertTo-Json -InputObject $hashBody - Description = $description - AccessToken = $AccessToken - TelemetryEventName = $MyInvocation.MyCommand.Name - TelemetryProperties = $telemetryProperties - } - - $result = Invoke-GHGraphQl @params - - if ($result -is [System.Management.Automation.ErrorRecord]) - { - $PSCmdlet.ThrowTerminatingError($result) - } - - $reviewDismissalActorIds += $result.data.user.id - } - } - - If ($PSBoundParameters.ContainsKey('DismissalTeams')) - { - Foreach($team in $DismissalTeams) - { - $hashbody = @{query = "query organization { organization(login: ""$OrganizationName"") " + - "{ team(slug: ""$team"") { id } } }"} - - $description = "Querying $OrganizationName organisation for team $team" - - Write-Debug -Message $description - - $params = @{ - Body = ConvertTo-Json -InputObject $hashBody - Description = $description - AccessToken = $AccessToken - TelemetryEventName = $MyInvocation.MyCommand.Name - TelemetryProperties = $telemetryProperties - } - - $result = Invoke-GHGraphQl @params - - if ($result -is [System.Management.Automation.ErrorRecord]) - { - $PSCmdlet.ThrowTerminatingError($result) - } - - if ($result.data.organization.team) - { - $reviewDismissalActorIds += $result.data.organization.team.id - } - else - { - $newErrorRecordParms = @{ - ErrorMessage = "Team $team not found in organization $OrganizationName" - ErrorId = 'DismissalTeamNotFound' - ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound - TargetObject = $team - } - $errorRecord = New-ErrorRecord @newErrorRecordParms - - $PSCmdlet.ThrowTerminatingError($errorRecord) - } - } - } + $mutationList += 'allowsForcePushes: ' + $AllowForcePushes.ToBool().ToString().ToLower() + } - $mutationList += 'restrictsReviewDismissals: true' - $mutationList += 'reviewDismissalActorIds: [ "' + ($reviewDismissalActorIds -join ('","')) + '" ]' + if ($PSBoundParameters.ContainsKey('AllowDeletions')) + { + $mutationList += 'allowsDeletions: ' + $AllowDeletions.ToBool().ToString().ToLower() } $mutationInput = $mutationList -join(',') diff --git a/USAGE.md b/USAGE.md index e3b9ad51..7c0c4002 100644 --- a/USAGE.md +++ b/USAGE.md @@ -64,6 +64,9 @@ * [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) + * [Getting a repository branch pattern protection rule](#getting-a-repository-branch-pattern-protection-rule) + * [Creating a repository branch pattern protection rule](#creating-a-repository-branch-pattern-protection-rule) + * [Removing a repository branch pattern protection rule](#removing-a-repository-branch-pattern-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) @@ -703,6 +706,24 @@ New-GitHubRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName Po Remove-GitHubRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchName master ``` +#### Getting a repository branch pattern protection rule + +```powershell +Get-GitHubRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchPatternName 'Release/**/*' +``` + +#### Creating a repository branch pattern protection rule + +```powershell +New-GitHubRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchPatternName 'Release/**/*' -RequiredApprovingReviewCount 1 -DismissStaleReviews -RequireStrictStatusChecks -StatusChecks 'CICheck' +``` + +#### Removing a repository branch pattern protection rule + +```powershell +Remove-GitHubRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchPatternName 'Release/**/*' +``` + ---------- ### Forks From c980fd7999e54e21da6cafb05c2b2909cc971710 Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Sun, 10 Jan 2021 11:03:46 +0000 Subject: [PATCH 04/33] Add tests --- Tests/GitHubBranches.tests.ps1 | 580 +++++++++++++++++++++++++++++++++ 1 file changed, 580 insertions(+) diff --git a/Tests/GitHubBranches.tests.ps1 b/Tests/GitHubBranches.tests.ps1 index 78768d12..000b15ba 100644 --- a/Tests/GitHubBranches.tests.ps1 +++ b/Tests/GitHubBranches.tests.ps1 @@ -782,6 +782,586 @@ try } } + Describe 'GitHubBranches\Get-GitHubRepositoryBranchPatternProtectionRule' { + BeforeAll { + $repoName = [Guid]::NewGuid().Guid + + $repo = New-GitHubRepository -RepositoryName $repoName + + $teamName = [Guid]::NewGuid().Guid + + $newGithubTeamParms = @{ + OrganizationName = $script:OrganizationName + TeamName = $teamName + } + + $team = New-GitHubTeam @newGithubTeamParms + + $setGitHubRepositoryTeamPermissionParms = @{ + Uri = $repo.svn_url + TeamSlug = $team.slug + Permission = 'Push' + } + + Set-GitHubRepositoryTeamPermission @setGitHubRepositoryTeamPermissionParms + } + + Context 'When getting branch pattern protection default options' { + BeforeAll { + $branchPatternName = [Guid]::NewGuid().Guid + '/**/*' + + New-GitHubRepositoryBranchPatternProtectionRule -Uri $repo.svn_url -BranchPatternName $branchPatternName + + $rule = Get-GitHubRepositoryBranchPatternProtectionRule -Uri $repo.svn_url -BranchPatternName $branchPatternName + } + + It 'Should have the expected type and addititional properties' { + $rule.PSObject.TypeNames[0] | Should -Be 'GitHub.BranchPatternProtectionRule' + $rule.pattern | Should -Be $branchPatternName + $rule.RepositoryUrl | Should -Be $repo.RepositoryUrl + $rule.requiresApprovingReviews | Should -BeFalse + $rule.requiredApprovingReviewCount | Should -BeNullOrEmpty + $rule.dismissesStaleReviews | Should -BeFalse + $rule.requiresCodeOwnerReviews | Should -BeFalse + $rule.restrictsReviewDismissals | Should -BeFalse + $rule.DismissalTeams | Should -BeNullOrEmpty + $rule.DismissalUsers | Should -BeNullOrEmpty + $rule.requiresStatusChecks | Should -BeFalse + $rule.requiresStrictStatusChecks | Should -BeTrue + $rule.requiredStatusCheckContexts | Should -BeNullOrEmpty + $rule.requiresCommitSignatures | Should -BeFalse + $rule.requiresLinearHistory | Should -BeFalse + $rule.isAdminEnforced | Should -BeFalse + $rule.restrictsPushes | Should -BeFalse + $rule.RestrictPushUsers | Should -BeNullOrEmpty + $rule.RestrictPushTeams | Should -BeNullOrEmpty + $rule.RestictPushApps | Should -BeNullOrEmpty + $rule.allowsForcePushes | Should -BeFalse + $rule.allowsDeletions | Should -BeFalse + } + } + + Context 'When getting base protection options' { + BeforeAll { + $branchPatternName = [Guid]::NewGuid().Guid + '/**/*' + + $newGitHubRepositoryBranchPatternProtectionParms = @{ + Uri = $repo.svn_url + BranchPatternName = $branchPatternName + RequireCommitSignatures = $true + RequireLinearHistory = $true + IsAdminEnforced = $true + RestrictPushUsers = $script:OwnerName + RestrictPushTeams = $TeamName + AllowForcePushes = $true + AllowDeletions = $true + } + + New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms + + $rule = Get-GitHubRepositoryBranchPatternProtectionRule -Uri $repo.svn_url -BranchPatternName $branchPatternName + } + + It 'Should have the expected type and addititional properties' { + $rule.PSObject.TypeNames[0] | Should -Be 'GitHub.BranchPatternProtectionRule' + $rule.pattern | Should -Be $branchPatternName + $rule.RepositoryUrl | Should -Be $repo.RepositoryUrl + $rule.requiresApprovingReviews | Should -BeFalse + $rule.requiredApprovingReviewCount | Should -BeNullOrEmpty + $rule.dismissesStaleReviews | Should -BeFalse + $rule.requiresCodeOwnerReviews | Should -BeFalse + $rule.restrictsReviewDismissals | Should -BeFalse + $rule.DismissalTeams | Should -BeNullOrEmpty + $rule.DismissalUsers | Should -BeNullOrEmpty + $rule.requiresStatusChecks | Should -BeFalse + $rule.requiresStrictStatusChecks | Should -BeTrue + $rule.requiredStatusCheckContexts | Should -BeNullOrEmpty + $rule.requiresCommitSignatures | Should -BeTrue + $rule.requiresLinearHistory | Should -BeTrue + $rule.isAdminEnforced | Should -BeTrue + $rule.restrictsPushes | Should -BeTrue + $rule.RestrictPushUsers.Count | Should -Be 1 + $rule.RestrictPushUsers | Should -Contain $script:OwnerName + $rule.RestrictPushTeams.Count | Should -Be 1 + $rule.RestrictPushTeams | Should -Contain $pushTeamName + $rule.RestrictPushApps | Should -BeNullOrEmpty + $rule.allowsForcePushes | Should -BeTrue + $rule.allowsDeletions | Should -BeTrue + } + } + + Context 'When getting required pull request reviews' { + BeforeAll { + $branchPatternName = [Guid]::NewGuid().Guid + '/**/*' + + $newGitHubRepositoryBranchPatternProtectionParms = @{ + Uri = $repo.svn_url + BranchPatternName = $branchPatternName + RequiredApprovingReviewCount = 1 + DismissStaleReviews = $true + RequireCodeOwnerReviews = $true + DismissalUsers = $script:OwnerName + DismissalTeams = $pushTeamName + } + + New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms + + $rule = Get-GitHubRepositoryBranchPatternProtectionRule -Uri $repo.svn_url -BranchPatternName $branchPatternName + } + + It 'Should have the expected type and addititional properties' { + $rule = Get-GitHubRepositoryBranchPatternProtectionRule -Uri $repo.svn_url -BranchPatternName $branchPatternName + $rule.PSObject.TypeNames[0] | Should -Be 'GitHub.BranchPatternProtectionRule' + $rule.pattern | Should -Be $branchPatternName + $rule.RepositoryUrl | Should -Be $repo.RepositoryUrl + $rule.requiresApprovingReviews | Should -BeTrue + $rule.requiredApprovingReviewCount | Should -Be 1 + $rule.dismissesStaleReviews | Should -BeTrue + $rule.requiresCodeOwnerReviews | Should -BeTrue + $rule.restrictsReviewDismissals | Should -BeTrue + $rule.DismissalTeams.Count | Should -Be 1 + $rule.DismissalTeams | Should -Contain $pushTeamName + $rule.DismissalUsers.Count | Should -Be 1 + $rule.DismissalUsers | Should -Contain $script:OwnerName + } + } + + Context 'When getting required status checks' { + BeforeAll { + $branchPatternName = [Guid]::NewGuid().Guid + '/**/*' + $statusChecks = 'test' + + $newGitHubRepositoryBranchPatternProtectionParms = @{ + Uri = $repo.svn_url + BranchPatternName = $branchPatternName + RequireStrictStatusChecks = $true + StatusChecks = $statusChecks + } + + New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms + + $rule = Get-GitHubRepositoryBranchPatternProtectionRule -Uri $repo.svn_url -BranchPatternName $branchPatternName + } + + It 'Should have the expected type and addititional properties' { + $rule.PSObject.TypeNames[0] | Should -Be 'GitHub.BranchPatternProtectionRule' + $rule.pattern | Should -Be $branchPatternName + $rule.RepositoryUrl | Should -Be $repo.RepositoryUrl + $rule.requiresStatusChecks | Should -BeTrue + $rule.requiresStrictStatusChecks | Should -BeTrue + $rule.requiredStatusCheckContexts | Should -Contain $statusChecks + } + } + + Context 'When specifying the "Uri" parameter through the pipeline' { + BeforeAll { + $rule = $repo | Get-GitHubRepositoryBranchPatternProtectionRule -BranchPatternName $branchPatternName + } + + It 'Should have the expected type and addititional properties' { + $rule.PSObject.TypeNames[0] | Should -Be 'GitHub.BranchPatternProtectionRule' + $rule.pattern | Should -Be $branchPatternName + $rule.RepositoryUrl | Should -Be $repo.RepositoryUrl + } + } + + AfterAll -ScriptBlock { + if (Get-Variable -Name repo -ErrorAction SilentlyContinue) + { + $repo | Remove-GitHubRepository -Force + } + } + } + + Describe 'GitHubBranches\New-GitHubRepositoryBranchPatternProtectionRule' { + BeforeAll { + $repoName = [Guid]::NewGuid().Guid + $newGitHubRepositoryParms = @{ + OrganizationName = $script:organizationName + RepositoryName = $repoName + } + + $repo = New-GitHubRepository @newGitHubRepositoryParms + + $pushTeamName = [Guid]::NewGuid().Guid + + $newGithubTeamParms = @{ + OrganizationName = $script:OrganizationName + TeamName = $pushTeamName + } + + $pushTeam = New-GitHubTeam @newGithubTeamParms + + $setGitHubRepositoryTeamPermissionParms = @{ + Uri = $repo.svn_url + TeamSlug = $pushTeam.slug + + Permission = 'Push' + } + + Set-GitHubRepositoryTeamPermission @setGitHubRepositoryTeamPermissionParms + + $pullTeamName = [Guid]::NewGuid().Guid + + $newGithubTeamParms = @{ + OrganizationName = $script:OrganizationName + TeamName = $pullTeamName + } + + $pullTeam = New-GitHubTeam @newGithubTeamParms + + $setGitHubRepositoryTeamPermissionParms = @{ + Uri = $repo.svn_url + TeamSlug = $pullTeam.slug + + Permission = 'Pull' + } + + Set-GitHubRepositoryTeamPermission @setGitHubRepositoryTeamPermissionParms + } + + Context 'When setting default protection options' { + BeforeAll { + $branchPatternName = [Guid]::NewGuid().Guid + '/**/*' + + $newGitHubRepositoryBranchPatternProtectionParms = @{ + Uri = $repo.svn_url + BranchPatternName = $branchPatternName + } + } + + It 'Should not throw' { + { New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms } | + Should -Not -Throw + } + + It 'Should have set the correct properties' { + $rule = Get-GitHubRepositoryBranchPatternProtectionRule -Uri $repo.svn_url -BranchPatternName $branchPatternName + + $rule.PSObject.TypeNames[0] | Should -Be 'GitHub.BranchPatternProtectionRule' + $rule.pattern | Should -Be $branchPatternName + $rule.RepositoryUrl | Should -Be $repo.RepositoryUrl + $rule.requiresApprovingReviews | Should -BeFalse + $rule.requiredApprovingReviewCount | Should -BeNullOrEmpty + $rule.dismissesStaleReviews | Should -BeFalse + $rule.requiresCodeOwnerReviews | Should -BeFalse + $rule.restrictsReviewDismissals | Should -BeFalse + $rule.DismissalTeams | Should -BeNullOrEmpty + $rule.DismissalUsers | Should -BeNullOrEmpty + $rule.requiresStatusChecks | Should -BeFalse + $rule.requiresStrictStatusChecks | Should -BeTrue + $rule.requiredStatusCheckContexts | Should -BeNullOrEmpty + $rule.requiresCommitSignatures | Should -BeFalse + $rule.requiresLinearHistory | Should -BeFalse + $rule.isAdminEnforced | Should -BeFalse + $rule.restrictsPushes | Should -BeFalse + $rule.RestrictPushUsers | Should -BeNullOrEmpty + $rule.RestrictPushTeams | Should -BeNullOrEmpty + $rule.RestictPushApps | Should -BeNullOrEmpty + $rule.allowsForcePushes | Should -BeFalse + $rule.allowsDeletions | Should -BeFalse + } + } + + Context 'When setting base protection options' { + BeforeAll { + $branchPatternName = [Guid]::NewGuid().Guid + '/**/*' + + $newGitHubRepositoryBranchPatternProtectionParms = @{ + Uri = $repo.svn_url + BranchPatternName = $branchPatternName + RequireCommitSignatures = $true + RequireLinearHistory = $true + IsAdminEnforced = $true + RestrictPushUsers = $script:OwnerName + RestrictPushTeams = $pushTeamName + AllowForcePushes = $true + AllowDeletions = $true + } + } + + It 'Should not throw' { + { New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms } | + Should -Not -Throw + } + + It 'Should have set the correct properties' { + $rule = Get-GitHubRepositoryBranchPatternProtectionRule -Uri $repo.svn_url -BranchPatternName $branchPatternName + + $rule.PSObject.TypeNames[0] | Should -Be 'GitHub.BranchPatternProtectionRule' + $rule.pattern | Should -Be $branchPatternName + $rule.RepositoryUrl | Should -Be $repo.RepositoryUrl + $rule.requiresApprovingReviews | Should -BeFalse + $rule.requiredApprovingReviewCount | Should -BeNullOrEmpty + $rule.dismissesStaleReviews | Should -BeFalse + $rule.requiresCodeOwnerReviews | Should -BeFalse + $rule.restrictsReviewDismissals | Should -BeFalse + $rule.DismissalTeams | Should -BeNullOrEmpty + $rule.DismissalUsers | Should -BeNullOrEmpty + $rule.requiresStatusChecks | Should -BeFalse + $rule.requiresStrictStatusChecks | Should -BeTrue + $rule.requiredStatusCheckContexts | Should -BeNullOrEmpty + $rule.requiresCommitSignatures | Should -BeTrue + $rule.requiresLinearHistory | Should -BeTrue + $rule.isAdminEnforced | Should -BeTrue + $rule.restrictsPushes | Should -BeTrue + $rule.RestrictPushUsers.Count | Should -Be 1 + $rule.RestrictPushUsers | Should -Contain $script:OwnerName + $rule.RestrictPushTeams.Count | Should -Be 1 + $rule.RestrictPushTeams | Should -Contain $pushTeamName + $rule.RestrictPushApps | Should -BeNullOrEmpty + $rule.allowsForcePushes | Should -BeTrue + $rule.allowsDeletions | Should -BeTrue + } + + Context 'When the Restrict Push Team does not exist in the organization' { + BeforeAll { + $branchPatternName = [Guid]::NewGuid().Guid + '/**/*' + $mockTeamName = 'MockTeam' + + $newGitHubRepositoryBranchPatternProtectionParms = @{ + Uri = $repo.svn_url + BranchPatternName = $branchPatternName + RestrictPushTeams = $mockTeamName + } + } + + It 'Should throw the correct exception' { + $errorMessage = "Team $mockTeamName not found in organization $OrganizationName" + { New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms } | + Should -Throw $errorMessage + } + } + + Context 'When the Restrict Push Team does not have push Permissions to the Repository' { + BeforeAll { + $branchPatternName = [Guid]::NewGuid().Guid + '/**/*' + + $newGitHubRepositoryBranchPatternProtectionParms = @{ + Uri = $repo.svn_url + BranchPatternName = $branchPatternName + RestrictPushTeams = $pullTeamName + } + } + + It 'Should throw the correct exception' { + $errorMessage = "Team $pullTeamName does not have push or maintain permissions on repository $repoName" + { New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms } | + Should -Throw $errorMessage + } + } + } + + Context 'When setting required pull request reviews' { + BeforeAll { + $branchPatternName = [Guid]::NewGuid().Guid + '/**/*' + + $newGitHubRepositoryBranchPatternProtectionParms = @{ + Uri = $repo.svn_url + BranchPatternName = $branchPatternName + RequiredApprovingReviewCount = 1 + DismissStaleReviews = $true + RequireCodeOwnerReviews = $true + DismissalUsers = $script:OwnerName + DismissalTeams = $pushTeamName + } + } + + It 'Should not throw' { + { New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms } | + Should -Not -Throw + } + + It 'Should have set the correct properties' { + $rule = Get-GitHubRepositoryBranchPatternProtectionRule -Uri $repo.svn_url -BranchPatternName $branchPatternName + $rule.PSObject.TypeNames[0] | Should -Be 'GitHub.BranchPatternProtectionRule' + $rule.pattern | Should -Be $branchPatternName + $rule.RepositoryUrl | Should -Be $repo.RepositoryUrl + $rule.requiresApprovingReviews | Should -BeTrue + $rule.requiredApprovingReviewCount | Should -Be 1 + $rule.dismissesStaleReviews | Should -BeTrue + $rule.requiresCodeOwnerReviews | Should -BeTrue + $rule.restrictsReviewDismissals | Should -BeTrue + $rule.DismissalTeams.Count | Should -Be 1 + $rule.DismissalTeams | Should -Contain $pushTeamName + $rule.DismissalUsers.Count | Should -Be 1 + $rule.DismissalUsers | Should -Contain $script:OwnerName + } + + Context 'When the Dismissal Team does not exist in the organization' { + BeforeAll { + $branchPatternName = [Guid]::NewGuid().Guid + '/**/*' + $mockTeamName = 'MockTeam' + + $newGitHubRepositoryBranchPatternProtectionParms = @{ + Uri = $repo.svn_url + BranchPatternName = $branchPatternName + DismissalTeams = $mockTeamName + } + } + + It 'Should throw the correct exception' { + $errorMessage = "Team $mockTeamName not found in organization $OrganizationName" + { New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms } | + Should -Throw $errorMessage + } + } + + Context 'When the Dismissal Team does not have write Permissions to the Repository' { + BeforeAll { + $branchPatternName = [Guid]::NewGuid().Guid + '/**/*' + + $newGitHubRepositoryBranchPatternProtectionParms = @{ + Uri = $repo.svn_url + BranchPatternName = $branchPatternName + DismissalTeams = $pullTeamName + } + } + + It 'Should throw the correct exception' { + $errorMessage = "Team $pullTeamName does not have push or maintain permissions on repository $repoName" + { New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms } | + Should -Throw $errorMessage + } + } + } + + Context 'When setting required status checks' { + BeforeAll { + $branchPatternName = [Guid]::NewGuid().Guid + '/**/*' + $statusChecks = 'test' + + $newGitHubRepositoryBranchPatternProtectionParms = @{ + Uri = $repo.svn_url + BranchPatternName = $branchPatternName + RequireStrictStatusChecks = $true + StatusChecks = $statusChecks + } + } + + It 'Should not throw' { + { New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms } | + Should -Not -Throw + } + + It 'Should have set the correct properties' { + $rule = Get-GitHubRepositoryBranchPatternProtectionRule -Uri $repo.svn_url -BranchPatternName $branchPatternName + + $rule.PSObject.TypeNames[0] | Should -Be 'GitHub.BranchPatternProtectionRule' + $rule.pattern | Should -Be $branchPatternName + $rule.RepositoryUrl | Should -Be $repo.RepositoryUrl + $rule.requiresStatusChecks | Should -BeTrue + $rule.requiresStrictStatusChecks | Should -BeTrue + $rule.requiredStatusCheckContexts | Should -Contain $statusChecks + } + } + + Context 'When the branch pattern rule already exists' { + BeforeAll { + $branchPatternName = [Guid]::NewGuid().Guid + '/**/*' + + $newGitHubRepositoryBranchPatternProtectionParms = @{ + Uri = $repo.svn_url + BranchPatternName = $branchPatternName + } + + $rule = New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms + } + + It 'Should throw the correct exception' { + $errorMessage = "Name already protected: $branchPatternName" + { New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms } | + Should -Throw $errorMessage + } + } + + Context 'When specifying the "Uri" parameter through the pipeline' { + BeforeAll { + $branchPatternName = [Guid]::NewGuid().Guid + '/**/*' + } + + It 'Should not throw' { + { $repo | New-GitHubRepositoryBranchPatternProtectionRule -BranchPatternName $branchPatternName } | + Should -Not -Throw + } + + It 'Should have set the correct properties' { + $rule = Get-GitHubRepositoryBranchPatternProtectionRule -Uri $repo.svn_url -BranchPatternName $branchPatternName + + $rule.PSObject.TypeNames[0] | Should -Be 'GitHub.BranchPatternProtectionRule' + $rule.pattern | Should -Be $branchPatternName + $rule.RepositoryUrl | Should -Be $repo.RepositoryUrl + } + } + + AfterAll -ScriptBlock { + if (Get-Variable -Name repo -ErrorAction SilentlyContinue) + { + $repo | Remove-GitHubRepository -Force + } + + if (Get-Variable -Name pushTeam -ErrorAction SilentlyContinue) + { + $pushTeam | Remove-GitHubTeam -Force + } + + if (Get-Variable -Name pullTeam -ErrorAction SilentlyContinue) + { + $pullTeam | Remove-GitHubTeam -Force + } + } + } + + Describe 'GitHubBranches\Remove-GitHubRepositoryBranchPatternProtectionRule' { + BeforeAll { + $repoName = [Guid]::NewGuid().Guid + + $repo = New-GitHubRepository -RepositoryName $repoName -AutoInit + } + + Context 'When removing GitHub repository branch pattern protection' { + BeforeAll { + $branchPatternName = [Guid]::NewGuid().Guid + '/**/*' + + New-GitHubRepositoryBranchPatternProtectionRule -Uri $repo.svn_url -BranchPatternName $branchPatternName + } + + It 'Should not throw' { + { Remove-GitHubRepositoryBranchPatternProtectionRule -Uri $repo.svn_url -BranchPatternName $branchPatternName -Force } | + Should -Not -Throw + } + + It 'Should have removed the protection rule' { + { Get-GitHubRepositoryBranchPatternProtectionRule -Uri $repo.svn_url -BranchPatternName $branchPatternName } | + Should -Throw + } + } + + Context 'When specifying the "Uri" parameter through the pipeline' { + BeforeAll { + $branchPatternName = [Guid]::NewGuid().Guid + '/**/*' + + $rule = New-GitHubRepositoryBranchPatternProtectionRule -Uri $repo.svn_url -BranchPatternName $branchPatternName + } + + It 'Should not throw' { + { $repo | Remove-GitHubRepositoryBranchPatternProtectionRule -BranchPatternName $branchPatternName -Force} | + Should -Not -Throw + } + + It 'Should have removed the protection rule' { + { Get-GitHubRepositoryBranchPatternProtectionRule -Uri $repo.svn_url -BranchPatternName $branchPatternName } | + Should -Throw + } + } + + AfterAll { + if (Get-Variable -Name repo -ErrorAction SilentlyContinue) + { + $repo | Remove-GitHubRepository -Force + } + } + } } finally { From a9dc929130c0ad5d42991d0a16b5d8085b35aff2 Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Sat, 23 Jan 2021 22:55:31 +0000 Subject: [PATCH 05/33] Update GitHubBranches --- GitHubBranches.ps1 | 109 ++++++++++++++++++++++++++------------------- 1 file changed, 63 insertions(+), 46 deletions(-) diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 index 93296738..3861199e 100644 --- a/GitHubBranches.ps1 +++ b/GitHubBranches.ps1 @@ -1248,7 +1248,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule $OwnerName = $elements.ownerName $RepositoryName = $elements.repositoryName - If ([System.String]::IsNullOrEmpty($OrganizationName)) + if ([System.String]::IsNullOrEmpty($OrganizationName)) { $OrganizationName = $OwnerName } @@ -1258,8 +1258,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule RepositoryName = (Get-PiiSafeString -PlainText $RepositoryName) } - $hashbody = @{query = "query repo { repository(name: ""$RepositoryName"" , " + - "owner: ""$OwnerName"") { id } }"} + $hashbody = @{query = "query repo { repository(name: ""$RepositoryName"", owner: ""$OwnerName"") { id } }"} Write-Debug -Message "Querying Repository $RepositoryName, Owner $OwnerName" @@ -1271,11 +1270,13 @@ filter New-GitHubRepositoryBranchPatternProtectionRule TelemetryProperties = $telemetryProperties } - $result = Invoke-GHGraphQl @params - - if ($result -is [System.Management.Automation.ErrorRecord]) + try { - $PSCmdlet.ThrowTerminatingError($result) + $result = Invoke-GHGraphQl @params + } + catch + { + $PSCmdlet.ThrowTerminatingError($_) } $repoId = $result.data.repository.id @@ -1313,9 +1314,9 @@ filter New-GitHubRepositoryBranchPatternProtectionRule { $reviewDismissalActorIds = @() - If ($PSBoundParameters.ContainsKey('DismissalUsers')) + if ($PSBoundParameters.ContainsKey('DismissalUsers')) { - Foreach($user in $DismissalUsers) + foreach($user in $DismissalUsers) { $hashbody = @{query = "query user { user(login: ""$user"") { id } }"} @@ -1331,20 +1332,22 @@ filter New-GitHubRepositoryBranchPatternProtectionRule TelemetryProperties = $telemetryProperties } - $result = Invoke-GHGraphQl @params - - if ($result -is [System.Management.Automation.ErrorRecord]) + try { - $PSCmdlet.ThrowTerminatingError($result) + $result = Invoke-GHGraphQl @params + } + catch + { + $PSCmdlet.ThrowTerminatingError($_) } $reviewDismissalActorIds += $result.data.user.id } } - If ($PSBoundParameters.ContainsKey('DismissalTeams')) + if ($PSBoundParameters.ContainsKey('DismissalTeams')) { - Foreach($team in $DismissalTeams) + foreach($team in $DismissalTeams) { $hashbody = @{query = "query organization { organization(login: ""$OrganizationName"") " + "{ team(slug: ""$team"") { id } } }"} @@ -1361,11 +1364,13 @@ filter New-GitHubRepositoryBranchPatternProtectionRule TelemetryProperties = $telemetryProperties } - $result = Invoke-GHGraphQl @params - - if ($result -is [System.Management.Automation.ErrorRecord]) + try + { + $result = Invoke-GHGraphQl @params + } + catch { - $PSCmdlet.ThrowTerminatingError($result) + $PSCmdlet.ThrowTerminatingError($_) } if ([System.String]::IsNullOrEmpty($result.data.organization.team)) @@ -1472,20 +1477,22 @@ filter New-GitHubRepositoryBranchPatternProtectionRule TelemetryProperties = $telemetryProperties } - $result = Invoke-GHGraphQl @params - - if ($result -is [System.Management.Automation.ErrorRecord]) + try + { + $result = Invoke-GHGraphQl @params + } + catch { - $PSCmdlet.ThrowTerminatingError($result) + $PSCmdlet.ThrowTerminatingError($_) } $restrictPushActorIds += $result.data.user.id } } - If ($PSBoundParameters.ContainsKey('RestrictPushTeams')) + if ($PSBoundParameters.ContainsKey('RestrictPushTeams')) { - Foreach($team in $RestrictPushTeams) + foreach($team in $RestrictPushTeams) { $hashbody = @{query = "query organization { organization(login: ""$OrganizationName"") " + "{ team(slug: ""$team"") { id } } }"} @@ -1502,11 +1509,13 @@ filter New-GitHubRepositoryBranchPatternProtectionRule TelemetryProperties = $telemetryProperties } - $result = Invoke-GHGraphQl @params - - if ($result -is [System.Management.Automation.ErrorRecord]) + try + { + $result = Invoke-GHGraphQl @params + } + catch { - $PSCmdlet.ThrowTerminatingError($result) + $PSCmdlet.ThrowTerminatingError($_) } if ([System.String]::IsNullOrEmpty($result.data.organization.team)) @@ -1553,7 +1562,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule if ($PSBoundParameters.ContainsKey('RestrictPushApps')) { - Foreach($app in $RestrictPushApps) + foreach ($app in $RestrictPushApps) { $hashbody = @{query = "query app { marketplaceListing(slug: ""$app"") { app { id } } }"} @@ -1569,11 +1578,13 @@ filter New-GitHubRepositoryBranchPatternProtectionRule TelemetryProperties = $telemetryProperties } - $result = Invoke-GHGraphQl @params - - if ($result -is [System.Management.Automation.ErrorRecord]) + try + { + $result = Invoke-GHGraphQl @params + } + catch { - $PSCmdlet.ThrowTerminatingError($result) + $PSCmdlet.ThrowTerminatingError($_) } if ($result.data.marketplaceListing) @@ -1768,11 +1779,13 @@ filter Get-GitHubRepositoryBranchPatternProtectionRule TelemetryProperties = $telemetryProperties } - $result = Invoke-GHGraphQl @params - - if ($result -is [System.Management.Automation.ErrorRecord]) + try { - $PSCmdlet.ThrowTerminatingError($result) + $result = Invoke-GHGraphQl @params + } + catch + { + $PSCmdlet.ThrowTerminatingError($_) } if ($result.data.repository.branchProtectionRules) @@ -1909,11 +1922,13 @@ filter Remove-GitHubRepositoryBranchPatternProtectionRule TelemetryProperties = $telemetryProperties } - $result = Invoke-GHGraphQl @params - - if ($result -is [System.Management.Automation.ErrorRecord]) + try { - $PSCmdlet.ThrowTerminatingError($result) + $result = Invoke-GHGraphQl @params + } + catch + { + $PSCmdlet.ThrowTerminatingError($_) } if ($result.data.repository.branchProtectionRules) @@ -1963,11 +1978,13 @@ filter Remove-GitHubRepositoryBranchPatternProtectionRule TelemetryProperties = $telemetryProperties } - $result = Invoke-GHGraphQl @params - - if ($result -is [System.Management.Automation.ErrorRecord]) + try { - $PSCmdlet.ThrowTerminatingError($result) + $result = Invoke-GHGraphQl @params + } + catch + { + $PSCmdlet.ThrowTerminatingError($_) } } From 77c8c49d74e5ac66a93ae8d1f59f441ac43ef99e Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Sat, 23 Jan 2021 22:56:51 +0000 Subject: [PATCH 06/33] Move Invoke-GHGraphQl to separate file --- GitHubCore.ps1 | 313 --------------------------------- GitHubGraphQl.ps1 | 363 +++++++++++++++++++++++++++++++++++++++ PowerShellForGitHub.psd1 | 1 + 3 files changed, 364 insertions(+), 313 deletions(-) create mode 100644 GitHubGraphQl.ps1 diff --git a/GitHubCore.ps1 b/GitHubCore.ps1 index 16bb8b03..3f37c402 100644 --- a/GitHubCore.ps1 +++ b/GitHubCore.ps1 @@ -744,319 +744,6 @@ function Invoke-GHRestMethodMultipleResult } } -function Invoke-GHGraphQl -{ -<# - .SYNOPSIS - A wrapper around Invoke-WebRequest that understands the GitHub GraphQL API. - - .DESCRIPTION - A very heavy wrapper around Invoke-WebRequest that understands the GitHub QraphQL API. - It also understands how to parse and handle errors from the REST calls. - - The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub - - .PARAMETER Description - A friendly description of the operation being performed for logging. - - .PARAMETER Body - This parameter forms the body of the request. It will be automatically - encoded to UTF8 and sent as Content Type: "application/json; charset=UTF-8" - - .PARAMETER AccessToken - If provided, this will be used as the AccessToken for authentication with the - REST Api as opposed to requesting a new one. - - .PARAMETER TelemetryEventName - If provided, the successful execution of this REST command will be logged to telemetry - using this event name. - - .PARAMETER TelemetryProperties - If provided, the successful execution of this REST command will be logged to telemetry - with these additional properties. This will be silently ignored if TelemetryEventName - is not provided as well. - - .PARAMETER TelemetryExceptionBucket - If provided, any exception that occurs will be logged to telemetry using this bucket. - It's possible that users will wish to log exceptions but not success (by providing - TelemetryEventName) if this is being executed as part of a larger scenario. If this - isn't provided, but TelemetryEventName *is* provided, then TelemetryEventName will be - used as the exception bucket value in the event of an exception. If neither is specified, - no bucket value will be used. - - .OUTPUTS - PSCustomObject - - .EXAMPLE - Invoke-GHGraphQl - - .NOTES - This wraps Invoke-WebRequest as opposed to Invoke-RestMethod because we want access - to the headers that are returned in the response, and Invoke-RestMethod drops those headers. -#> - [CmdletBinding()] - [OutputType([System.Management.Automation.ErrorRecord])] - param( - [string] $Description, - - [Parameter(Mandatory)] - [string] $Body, - - [string] $AccessToken, - - [string] $TelemetryEventName = $null, - - [hashtable] $TelemetryProperties = @{}, - - [string] $TelemetryExceptionBucket = $null - ) - - Invoke-UpdateCheck - - # Telemetry-related - $stopwatch = New-Object -TypeName System.Diagnostics.Stopwatch - $localTelemetryProperties = @{} - $TelemetryProperties.Keys | ForEach-Object { $localTelemetryProperties[$_] = $TelemetryProperties[$_] } - $errorBucket = $TelemetryExceptionBucket - if ([String]::IsNullOrEmpty($errorBucket)) - { - $errorBucket = $TelemetryEventName - } - - $stopwatch.Start() - - $hostName = $(Get-GitHubConfiguration -Name "ApiHostName") - - if ($hostName -eq 'github.com') - { - $url = "https://api.$hostName/graphql" - } - else - { - $url = "https://$hostName/api/v3/graphql" - } - - $headers = @{ - 'User-Agent' = 'PowerShellForGitHub' - } - - $AccessToken = Get-AccessToken -AccessToken $AccessToken - if (-not [String]::IsNullOrEmpty($AccessToken)) - { - $headers['Authorization'] = "token $AccessToken" - } - - Write-Log -Message $Description -Level Verbose - Write-Log -Message "Accessing [$Method] $url [Timeout = $(Get-GitHubConfiguration -Name WebRequestTimeoutSec))]" -Level Verbose - - $bodyAsBytes = [System.Text.Encoding]::UTF8.GetBytes($Body) - - $params = @{ - Uri = $url - Method = 'Post' - Headers = $headers - Body = $bodyAsBytes - UseDefaultCredentials = $true - UseBasicParsing = $true - TimeoutSec = Get-GitHubConfiguration -Name WebRequestTimeoutSec - } - - if (Get-GitHubConfiguration -Name LogRequestBody) - { - Write-Log -Message $Body -Level Verbose - } - - # Disable Progress Bar in function scope during Invoke-WebRequest - $ProgressPreference = 'SilentlyContinue' - - # Save Current Security Protocol - $originalSecurityProtocol = [Net.ServicePointManager]::SecurityProtocol - - # Enforce TLS v1.2 Security Protocol - [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12 - - try { - $result = Invoke-WebRequest @params - } - catch - { - Write-Debug -Message "Processing Exception $($_.Exception.PSTypeNames[0])" - - # PowerShell 5 Invoke-WebRequest returns a 'System.Net.WebException' object on error - # PowerShell 6+ Invoke-WebRequest returns a 'Microsoft.PowerShell.Commands.HttpResponseException' object on error - if ($_.Exception.PSTypeNames[0] -eq 'System.Net.WebException' -or - $_.Exception.PSTypeNames[0] -eq 'Microsoft.PowerShell.Commands.HttpResponseException') - { - $ex = $_.Exception - $message = $ex.Message - - if ($ex.Exception.Response -is [System.Net.WebResponse]) - { - $statusCode = $ex.Response.StatusCode.value__ - - if ($ex.Response.PSTypeNames[0] -eq 'System.Net.Http.HttpResponseMessage') - { - $statusDescription = $ex.Response.ReasonPhrase - } - elseif ($ex.Response.PSTypeNames[0] -eq 'System.Net.HttpWebResponse') - { - $statusDescription = $ex.Response.StatusDescription - } - else { - $statusDescription = '' - } - - if ($ex.Response.Headers.Count -gt 0) - { - $requestId = $ex.Response.Headers['X-GitHub-Request-Id'] - } - } - - $innerMessage = $_.ErrorDetails.Message - try - { -# $rawContent = Get-HttpWebResponseContent -WebResponse $ex.Response - } - catch - { - Write-Log -Message "Unable to retrieve the raw HTTP Web Response:" -Exception $_ -Level Warning - } - } - else - { - Set-TelemetryException -Exception $_.Exception -ErrorBucket $errorBucket -Properties $localTelemetryProperties - - $newErrorRecordParms = @{ - ErrorMessage = $_.ErrorDetails.Message - ErrorId = $statusCode - ErrorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation - TargetObject = $body - } - $errorRecord = New-ErrorRecord @newErrorRecordParms - - if ($PSCmdlet.CommandOrigin -eq [System.Management.Automation.CommandOrigin]::Runspace) - { - $PSCmdlet.ThrowTerminatingError($errorRecord) - } - else - { - return $errorRecord - } - } - - $output = @() - $output += $message - - if (-not [string]::IsNullOrEmpty($statusCode)) - { - $output += "$statusCode | $($statusDescription.Trim())" - } - - if (-not [string]::IsNullOrEmpty($innerMessage)) - { - try - { - $innerMessageJson = ($innerMessage | ConvertFrom-Json) - } - catch [System.ArgumentException] - { - # Will be thrown if $innerMessage isn't JSON content - $innerMessageJson = $innerMessage.Trim() - } - - if ($innerMessageJson -is [String]) - { - $output += $innerMessageJson.Trim() - } - elseif (-not [String]::IsNullOrWhiteSpace($innerMessageJson.message)) - { - $output += "$($innerMessageJson.message.Trim()) | $($innerMessageJson.documentation_url.Trim())" - if ($innerMessageJson.details) - { - $output += "$($innerMessageJson.details | Format-Table | Out-String)" - } - } - else - { - # In this case, it's probably not a normal message from the API - $output += ($innerMessageJson | Out-String) - } - } - - # It's possible that the API returned JSON content in its error response. - if (-not [String]::IsNullOrWhiteSpace($rawContent)) - { - $output += $rawContent - } - - if (-not [String]::IsNullOrEmpty($requestId)) - { - $localTelemetryProperties['RequestId'] = $requestId - $message = 'RequestId: ' + $requestId - $output += $message - Write-Log -Message $message -Level Verbose - } - - $newLineOutput = ($output -join [Environment]::NewLine) - Set-TelemetryException -Exception $ex -ErrorBucket $errorBucket -Properties $localTelemetryProperties - - $newErrorRecordParms = @{ - ErrorMessage = $newLineOutput - ErrorId = $statusCode - ErrorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation - TargetObject = $body - } - $errorRecord = New-ErrorRecord @newErrorRecordParms - - if ($PSCmdlet.CommandOrigin -eq [System.Management.Automation.CommandOrigin]::Runspace) - { - $PSCmdlet.ThrowTerminatingError($errorRecord) - } - else - { - return $errorRecord - } - } - finally { - # Restore original security protocol - [Net.ServicePointManager]::SecurityProtocol = $originalSecurityProtocol - - # Record the telemetry for this event. - $stopwatch.Stop() - if (-not [String]::IsNullOrEmpty($TelemetryEventName)) - { - $telemetryMetrics = @{ 'Duration' = $stopwatch.Elapsed.TotalSeconds } - Set-TelemetryEvent -EventName $TelemetryEventName -Properties $localTelemetryProperties -Metrics $telemetryMetrics - } - } - - $graphQlResult = $result.Content | ConvertFrom-Json - - if ($graphQlResult.errors) - { - write-debug ($graphqlResult.errors|Fl|out-string) - $newErrorRecordParms = @{ - ErrorMessage = $graphQlResult.errors[0].message - ErrorId = $graphQlResult.errors[0].type - ErrorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation - } - $errorRecord = New-ErrorRecord @newErrorRecordParms - - if ($PSCmdlet.CommandOrigin -eq [System.Management.Automation.CommandOrigin]::Runspace) - { - $PSCmdlet.ThrowTerminatingError($errorRecord) - } - else - { - return $errorRecord - } - } - else - { - return $graphQlResult - } -} - filter Split-GitHubUri { <# diff --git a/GitHubGraphQl.ps1 b/GitHubGraphQl.ps1 new file mode 100644 index 00000000..c63c5582 --- /dev/null +++ b/GitHubGraphQl.ps1 @@ -0,0 +1,363 @@ +function Invoke-GHGraphQl +{ +<# + .SYNOPSIS + A wrapper around Invoke-WebRequest that understands the GitHub GraphQL API. + + .DESCRIPTION + A very heavy wrapper around Invoke-WebRequest that understands the GitHub QraphQL API. + It also understands how to parse and handle errors from the GraphQL calls. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Description + A friendly description of the operation being performed for logging. + + .PARAMETER Body + This parameter forms the body of the request. It will be automatically + encoded to UTF8 and sent as Content Type: "application/json; charset=UTF-8" + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + GraphQL Api as opposed to requesting a new one. + + .PARAMETER TelemetryEventName + If provided, the successful execution of this GraphQL command will be logged to telemetry + using this event name. + + .PARAMETER TelemetryProperties + If provided, the successful execution of this GraphQL command will be logged to telemetry + with these additional properties. This will be silently ignored if TelemetryEventName + is not provided as well. + + .PARAMETER TelemetryExceptionBucket + If provided, any exception that occurs will be logged to telemetry using this bucket. + It's possible that users will wish to log exceptions but not success (by providing + TelemetryEventName) if this is being executed as part of a larger scenario. If this + isn't provided, but TelemetryEventName *is* provided, then TelemetryEventName will be + used as the exception bucket value in the event of an exception. If neither is specified, + no bucket value will be used. + + .OUTPUTS + PSCustomObject + + .EXAMPLE + Invoke-GHGraphQl + + .NOTES + This wraps Invoke-WebRequest as opposed to Invoke-RestMethod because we want access + to the headers that are returned in the response, and Invoke-RestMethod drops those headers. +#> + [CmdletBinding()] + [OutputType([System.Management.Automation.ErrorRecord])] + param( + [string] $Description, + + [Parameter(Mandatory)] + [string] $Body, + + [string] $AccessToken, + + [string] $TelemetryEventName = $null, + + [hashtable] $TelemetryProperties = @{}, + + [string] $TelemetryExceptionBucket = $null + ) + + Invoke-UpdateCheck + + # Telemetry-related + $stopwatch = New-Object -TypeName System.Diagnostics.Stopwatch + $localTelemetryProperties = @{} + $TelemetryProperties.Keys | ForEach-Object { $localTelemetryProperties[$_] = $TelemetryProperties[$_] } + $errorBucket = $TelemetryExceptionBucket + if ([String]::IsNullOrEmpty($errorBucket)) + { + $errorBucket = $TelemetryEventName + } + + $stopwatch.Start() + + $hostName = $(Get-GitHubConfiguration -Name 'ApiHostName') + + if ($hostName -eq 'github.com') + { + $url = "https://api.$hostName/graphql" + } + else + { + $url = "https://$hostName/api/v3/graphql" + } + + $headers = @{ + 'User-Agent' = 'PowerShellForGitHub' + } + + $AccessToken = Get-AccessToken -AccessToken $AccessToken + if (-not [String]::IsNullOrEmpty($AccessToken)) + { + $headers['Authorization'] = "token $AccessToken" + } + + $timeOut = Get-GitHubConfiguration -Name WebRequestTimeoutSec + + Write-Log -Message $Description -Level Verbose + Write-Log -Message "Accessing [$Method] $url [Timeout = $timeOut]" -Level Verbose + + $bodyAsBytes = [System.Text.Encoding]::UTF8.GetBytes($Body) + + $params = @{ + Uri = $url + Method = 'Post' + Headers = $headers + Body = $bodyAsBytes + UseDefaultCredentials = $true + UseBasicParsing = $true + TimeoutSec = Get-GitHubConfiguration -Name WebRequestTimeoutSec + } + + if (Get-GitHubConfiguration -Name LogRequestBody) + { + Write-Log -Message $Body -Level Verbose + } + + # Disable Progress Bar in function scope during Invoke-WebRequest + $ProgressPreference = 'SilentlyContinue' + + # Save Current Security Protocol + $originalSecurityProtocol = [Net.ServicePointManager]::SecurityProtocol + + # Enforce TLS v1.2 Security Protocol + [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12 + + try { + $result = Invoke-WebRequest @params + } + catch + { + $ex = $_.Exception + + <# + PowerShell 5 Invoke-WebRequest returns a 'System.Net.WebException' object on error. + PowerShell 6+ Invoke-WebRequest returns a 'Microsoft.PowerShell.Commands.HttpResponseException' or + a 'System.Net.Http.HttpRequestException' object on error. + #> + + if ($ex.PSTypeNames[0] -eq 'System.Net.Http.HttpRequestException') + { + Write-Debug -Message "Processing PowerShell Core 'System.Net.Http.HttpRequestException'" + + $newErrorRecordParms = @{ + ErrorMessage = $ex.Message + ErrorId = $_.FullyQualifiedErrorId + ErrorCategory = $_.CategoryInfo.Category + TargetObject = $_.TargetObject + } + $errorRecord = New-ErrorRecord @newErrorRecordParms + + Write-Log -Exception $errorRecord -Level Error + Set-TelemetryException -Exception $ex -ErrorBucket $errorBucket -Properties $localTelemetryProperties + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + elseif ($ex.PSTypeNames[0] -eq 'Microsoft.PowerShell.Commands.HttpResponseException' -or + $ex.PSTypeNames[0] -eq 'System.Net.WebException') + { + Write-Debug -Message "Processing '$($ex.PSTypeNames[0])'" + + $errorMessage = @() + $errorMessage += $ex.Message + + $errorDetailsMessage = $_.ErrorDetails.Message + + if (-not [string]::IsNullOrEmpty($errorDetailsMessage)) + { + Write-Debug -Message "Processing Error Details message '$errorDetailsMessage'" + + try { + Write-Debug -Message 'Checking Error Details message for JSON content' + + $errorDetailsMessageJson = $errorDetailsMessage | ConvertFrom-Json + } + catch [System.ArgumentException] + { + # Will be thrown if $errorDetailsMessage isn't JSON content + Write-Debug -Message 'No Error Details Message JSON content Found' + + $errorDetailsMessageJson = $false + } + + if ($errorDetailsMessageJson) + { + Write-Debug -Message 'Adding Error Details Message JSON content to output' + Write-Debug -Message "Error Details Message: $($errorDetailsMessageJson.message)" + Write-Debug -Message "Error Details Documentation URL: $($errorDetailsMessageJson.documentation_url)" + + $errorMessage += ($($errorDetailsMessageJson.message.Trim()) + + " | $($errorDetailsMessageJson.documentation_url.Trim())") + + if ($errorDetailsMessageJson.details) + { + $errorMessage += $errorDetailsMessageJson.details | Format-Table | Out-String + } + } + else + { + # In this case, it's probably not a normal message from the API + Write-Debug -Message 'Adding Error Details Message String to output' + + $errorMessage += $_.ErrorDetails.Message | Out-String + } + } + + if (-not [System.String]::IsNullOrEmpty($ex.Response)) + { + Write-Debug -Message "Processing '$($ex.Response.PSTypeNames[0])' Object" + + <# + PowerShell 5.x returns a 'System.Net.HttpWebResponse' exception response object and + PowerShell 6+ returns a 'System.Net.Http.HttpResponseMessage' exception response object. + #> + + $requestId = '' + + if ($ex.Response.PSTypeNames[0] -eq 'System.Net.Http.HttpResponseMessage') + { + $requestId = ($ex.Response.Headers | Where-Object -Property Key -eq 'X-GitHub-Request-Id').Value + } + elseif ($ex.Response.PSTypeNames[0] -eq 'System.Net.HttpWebResponse') + { + if ($ex.Response.Headers.Count -gt 0 -and + -not [System.String]::IsNullOrEmpty($ex.Response.Headers['X-GitHub-Request-Id'])) + { + $requestId = $ex.Response.Headers['X-GitHub-Request-Id'] + } + } + + if (-not [System.String]::IsNullOrEmpty($requestId)) + { + Write-Debug -Message "GitHub RequestID '$requestId' in response header" + + $localTelemetryProperties['RequestId'] = $requestId + $requestIdMessage += "RequestId: $requestId" + $errorMessage += $requestIdMessage + + Write-Log -Message $requestIdMessage -Level Verbose + } + } + + $newErrorRecordParms = @{ + ErrorMessage = $errorMessage -join [Environment]::NewLine + ErrorId = $_.FullyQualifiedErrorId + ErrorCategory = $_.CategoryInfo.Category + TargetObject = $Body + } + $errorRecord = New-ErrorRecord @newErrorRecordParms + + Write-Log -Exception $errorRecord -Level Error + Set-TelemetryException -Exception $ex -ErrorBucket $errorBucket -Properties $localTelemetryProperties + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + else + { + Write-Debug -Message "Processing Other Exception '$($ex.PSTypeNames[0])'" + + $newErrorRecordParms = @{ + ErrorMessage = $ex.Message + ErrorId = $_.FullyQualifiedErrorId + ErrorCategory = $_.CategoryInfo.Category + TargetObject = $body + } + $errorRecord = New-ErrorRecord @newErrorRecordParms + + Write-Log -Exception $errorRecord -Level Error + Set-TelemetryException -Exception $ex -ErrorBucket $errorBucket -Properties $localTelemetryProperties + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + } + finally { + Write-Debug -Message "Processing Invoke-WebRequest 'finally' block" + + # Restore original security protocol + [Net.ServicePointManager]::SecurityProtocol = $originalSecurityProtocol + + # Record the telemetry for this event. + $stopwatch.Stop() + if (-not [String]::IsNullOrEmpty($TelemetryEventName)) + { + $telemetryMetrics = @{ 'Duration' = $stopwatch.Elapsed.TotalSeconds } + Set-TelemetryEvent -EventName $TelemetryEventName -Properties $localTelemetryProperties -Metrics $telemetryMetrics + } + } + + $graphQlResult = $result.Content | ConvertFrom-Json + + if ($graphQlResult.errors) + { + Write-Debug -Message "GraphQl Error: $($graphQLResult.errors | Out-String)" + + if (-not [System.String]::IsNullOrEmpty($graphQlResult.errors[0].type)) + { + $errorId = $graphQlResult.errors[0].type + switch ($graphQlResult.errors[0].type) + { + 'NOT_FOUND' + { + Write-Debug -Message "GraphQl Error Type: $($graphQlResult.errors[0].type)" + + $errorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound + } + + Default + { + Write-Debug -Message "GraphQL Unknown Error Type: $($graphQlResult.errors[0].type)" + + $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation + } + } + } + else + { + Write-Debug -Message "GraphQl Unspecified Error" + + $errorId = 'UnspecifiedError' + $errorCategory = [System.Management.Automation.ErrorCategory]::NotSpecified + } + + $errorMessage = @() + $errorMessage += "GraphQl Error: $($graphQlResult.errors[0].message)" + + if ($result.Headers.Count -gt 0 -and + -not [System.String]::IsNullOrEmpty($result.Headers['X-GitHub-Request-Id'])) + { + $requestId = $result.Headers['X-GitHub-Request-Id'] + + Write-Debug -Message "GitHub RequestID '$requestId' in response header" + + $requestIdMessage += "RequestId: $requestId" + $errorMessage += $requestIdMessage + + Write-Log -Message $requestIdMessage -Level Verbose + } + + $newErrorRecordParms = @{ + ErrorMessage = $errorMessage -join [Environment]::NewLine + ErrorId = $errorId + ErrorCategory = $errorCategory + TargetObject = $Body + } + $errorRecord = New-ErrorRecord @newErrorRecordParms + + Write-Log -Exception $errrorRecord -Level Error + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + else + { + Write-Debug -Message "Returning GraphQl result '$graphQLResult'" + return $graphQlResult + } +} diff --git a/PowerShellForGitHub.psd1 b/PowerShellForGitHub.psd1 index db633e70..49ed0f35 100644 --- a/PowerShellForGitHub.psd1 +++ b/PowerShellForGitHub.psd1 @@ -34,6 +34,7 @@ 'GitHubAssignees.ps1', 'GitHubBranches.ps1', 'GitHubCore.ps1', + 'GitHubGraphQl.ps1', 'GitHubContents.ps1', 'GitHubEvents.ps1', 'GitHubGistComments.ps1', From 5bcef0fe2103536e0a5726b0d9fcf6cc4b56f4f3 Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Sat, 23 Jan 2021 22:57:14 +0000 Subject: [PATCH 07/33] Remove Write-Log from New-ErrorRecord --- Helpers.ps1 | 2 -- 1 file changed, 2 deletions(-) diff --git a/Helpers.ps1 b/Helpers.ps1 index afca8357..d915140c 100644 --- a/Helpers.ps1 +++ b/Helpers.ps1 @@ -709,8 +709,6 @@ function New-ErrorRecord [System.Management.Automation.PSObject] $TargetObject ) - Write-Log -Message $ErrorMessage -Level Error - $exception = New-Object -TypeName System.Exception -ArgumentList $ErrorMessage $errorRecordArgumentList = $exception, $ErrorId, $ErrorCategory, $TargetObject $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $errorRecordArgumentList From 33fe88681e310cb7f3cb61dd293b823e9cac662b Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Sat, 23 Jan 2021 22:57:59 +0000 Subject: [PATCH 08/33] Update tests --- Tests/Common.ps1 | 1 + Tests/GitHubBranches.tests.ps1 | 32 +++-- Tests/GitHubGraphQl.Tests.ps1 | 224 +++++++++++++++++++++++++++++++++ 3 files changed, 240 insertions(+), 17 deletions(-) create mode 100644 Tests/GitHubGraphQl.Tests.ps1 diff --git a/Tests/Common.ps1 b/Tests/Common.ps1 index d7ced214..fc89f781 100644 --- a/Tests/Common.ps1 +++ b/Tests/Common.ps1 @@ -41,6 +41,7 @@ function Initialize-CommonTestSetup [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "", Justification="Needed to configure with the stored, encrypted string value in Azure DevOps.")] param() + $script:moduleName = 'PowerShellForGitHub' $moduleRootPath = Split-Path -Path $PSScriptRoot -Parent $settingsPath = Join-Path -Path $moduleRootPath -ChildPath 'Tests/Config/Settings.ps1' . $settingsPath diff --git a/Tests/GitHubBranches.tests.ps1 b/Tests/GitHubBranches.tests.ps1 index 000b15ba..5b2d543f 100644 --- a/Tests/GitHubBranches.tests.ps1 +++ b/Tests/GitHubBranches.tests.ps1 @@ -786,7 +786,11 @@ try BeforeAll { $repoName = [Guid]::NewGuid().Guid - $repo = New-GitHubRepository -RepositoryName $repoName + $newGitHubRepositoryParms = @{ + OrganizationName = $script:organizationName + RepositoryName = $repoName + } + $repo = New-GitHubRepository @newGitHubRepositoryParms $teamName = [Guid]::NewGuid().Guid @@ -794,7 +798,6 @@ try OrganizationName = $script:OrganizationName TeamName = $teamName } - $team = New-GitHubTeam @newGithubTeamParms $setGitHubRepositoryTeamPermissionParms = @{ @@ -802,7 +805,6 @@ try TeamSlug = $team.slug Permission = 'Push' } - Set-GitHubRepositoryTeamPermission @setGitHubRepositoryTeamPermissionParms } @@ -856,7 +858,6 @@ try AllowForcePushes = $true AllowDeletions = $true } - New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms $rule = Get-GitHubRepositoryBranchPatternProtectionRule -Uri $repo.svn_url -BranchPatternName $branchPatternName @@ -883,7 +884,7 @@ try $rule.RestrictPushUsers.Count | Should -Be 1 $rule.RestrictPushUsers | Should -Contain $script:OwnerName $rule.RestrictPushTeams.Count | Should -Be 1 - $rule.RestrictPushTeams | Should -Contain $pushTeamName + $rule.RestrictPushTeams | Should -Contain $teamName $rule.RestrictPushApps | Should -BeNullOrEmpty $rule.allowsForcePushes | Should -BeTrue $rule.allowsDeletions | Should -BeTrue @@ -901,9 +902,8 @@ try DismissStaleReviews = $true RequireCodeOwnerReviews = $true DismissalUsers = $script:OwnerName - DismissalTeams = $pushTeamName + DismissalTeams = $teamName } - New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms $rule = Get-GitHubRepositoryBranchPatternProtectionRule -Uri $repo.svn_url -BranchPatternName $branchPatternName @@ -920,7 +920,7 @@ try $rule.requiresCodeOwnerReviews | Should -BeTrue $rule.restrictsReviewDismissals | Should -BeTrue $rule.DismissalTeams.Count | Should -Be 1 - $rule.DismissalTeams | Should -Contain $pushTeamName + $rule.DismissalTeams | Should -Contain $teamName $rule.DismissalUsers.Count | Should -Be 1 $rule.DismissalUsers | Should -Contain $script:OwnerName } @@ -937,7 +937,6 @@ try RequireStrictStatusChecks = $true StatusChecks = $statusChecks } - New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms $rule = Get-GitHubRepositoryBranchPatternProtectionRule -Uri $repo.svn_url -BranchPatternName $branchPatternName @@ -970,17 +969,22 @@ try { $repo | Remove-GitHubRepository -Force } + + if (Get-Variable -Name team -ErrorAction SilentlyContinue) + { + $team | Remove-GitHubTeam -Force + } } } Describe 'GitHubBranches\New-GitHubRepositoryBranchPatternProtectionRule' { BeforeAll { $repoName = [Guid]::NewGuid().Guid + $newGitHubRepositoryParms = @{ OrganizationName = $script:organizationName RepositoryName = $repoName } - $repo = New-GitHubRepository @newGitHubRepositoryParms $pushTeamName = [Guid]::NewGuid().Guid @@ -989,16 +993,13 @@ try OrganizationName = $script:OrganizationName TeamName = $pushTeamName } - $pushTeam = New-GitHubTeam @newGithubTeamParms $setGitHubRepositoryTeamPermissionParms = @{ Uri = $repo.svn_url TeamSlug = $pushTeam.slug - Permission = 'Push' } - Set-GitHubRepositoryTeamPermission @setGitHubRepositoryTeamPermissionParms $pullTeamName = [Guid]::NewGuid().Guid @@ -1007,7 +1008,6 @@ try OrganizationName = $script:OrganizationName TeamName = $pullTeamName } - $pullTeam = New-GitHubTeam @newGithubTeamParms $setGitHubRepositoryTeamPermissionParms = @{ @@ -1016,7 +1016,6 @@ try Permission = 'Pull' } - Set-GitHubRepositoryTeamPermission @setGitHubRepositoryTeamPermissionParms } @@ -1264,7 +1263,6 @@ try Uri = $repo.svn_url BranchPatternName = $branchPatternName } - $rule = New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms } @@ -1316,7 +1314,7 @@ try BeforeAll { $repoName = [Guid]::NewGuid().Guid - $repo = New-GitHubRepository -RepositoryName $repoName -AutoInit + $repo = New-GitHubRepository -RepositoryName $repoName } Context 'When removing GitHub repository branch pattern protection' { diff --git a/Tests/GitHubGraphQl.Tests.ps1 b/Tests/GitHubGraphQl.Tests.ps1 new file mode 100644 index 00000000..16e860c4 --- /dev/null +++ b/Tests/GitHubGraphQl.Tests.ps1 @@ -0,0 +1,224 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# +.Synopsis + Tests for GitHubGraphQl.ps1 module +#> + +[CmdletBinding()] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', + Justification='Suppress false positives in Pester code blocks')] +param() + +# This is common test code setup logic for all Pester test files +$moduleRootPath = Split-Path -Path $PSScriptRoot -Parent +. (Join-Path -Path $moduleRootPath -ChildPath 'Tests\Common.ps1') + +try +{ + Describe 'GitHubCore/Invoke-GHGraphQl' { + BeforeAll { + $Description = 'description' + $AccessToken='' + $TelemetryEventName= $null + $TelemetryProperties = @{} + $TelemetryExceptionBucket = $null + + Mock -CommandName Invoke-UpdateCheck -ModuleName $script:moduleName + } + + Context 'When a valid query is specified' { + BeforeAll { + $testBody = '{ "query": "query login { viewer { login } }" }' + } + + It 'Should return the expected result' { + $invokeGHGraphQLParms = @{ + Body = $testBody + } + $result = Invoke-GHGraphQl @invokeGHGraphQLParms + + $result.data.viewer.login | Should -Be $script:ownerName + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Invoke-UpdateCheck ` + -ModuleName $script:moduleName ` + -Exactly -Times 1 + } + } + + Context 'When there is a Web/HTTP Request exception in Invoke-WebRequest' { + BeforeAll { + $testHostName = 'invalidhostname' + $testBody = 'testBody' + + if ($PSVersionTable.PSEdition -eq 'Core') { + $exceptionMessage = 'No such host is known' + $categoryInfo = 'InvalidOperation' + $targetName = "*$testHostName*" + } + else { + $exceptionMessage = "The remote name could not be resolved: '$testHostName'" + $categoryInfo = 'NotSpecified' + $targetName = $testBody + } + + Mock -CommandName Get-GitHubConfiguration -ModuleName $script:moduleName ` + -ParameterFilter { $Name -eq 'ApiHostName' } ` + -MockWith { 'invalidhostname' } + } + + It 'Should throw the correct exception' { + $invokeGHGraphQLParms = @{ + Body = $testBody + } + { Invoke-GHGraphQl @invokeGHGraphQlParms } | + Should -Throw $exceptionMessage + + $Error[0].CategoryInfo.Category | Should -Be $categoryInfo + $Error[0].CategoryInfo.TargetName | Should -BeLike $targetName + $Error[0].FullyQualifiedErrorId | Should -BeLike '*Invoke-GHGraphQl' + } + } + + Context 'When there is a Web/HTTP Response exception in Invoke-WebRequest' { + Context 'When there is invalid JSON in the request body' { + BeforeAll { + $testBody = 'InvalidJson' + + if ($PSVersionTable.PSEdition -eq 'Core') { + $exceptionMessage1 = '*Response status code does not indicate success: 400 (Bad Request)*' + } + else { + $exceptionMessage1 = '*The remote server returned an error: (400) Bad Request*' + } + + $exceptionMessage2 = '*Problems parsing JSON | https://docs.github.com/graphql*' + } + + It 'Should throw the correct exception' { + $invokeGHGraphQLParms = @{ + Body = $testBody + } + { Invoke-GHGraphQl @invokeGHGraphQlParms } | + Should -Throw + + $Error[0].Exception.Message | Should -BeLike $exceptionMessage1 + $Error[0].Exception.Message | Should -BeLike $exceptionMessage2 + $Error[0].Exception.Message | Should -BeLike '*RequestId:*' + $Error[0].CategoryInfo.Category | Should -Be 'InvalidOperation' + $Error[0].CategoryInfo.TargetName | Should -Be $testBody + $Error[0].FullyQualifiedErrorId | Should -BeLike '*Invoke-GHGraphQl' + } + } + + Context 'When the query user is not authenticated' { + BeforeAll { + $testBody = '{ "query": "query login { viewer { login } }" }' + + if ($PSVersionTable.PSEdition -eq 'Core') { + $exceptionMessage1 = '*Response status code does not indicate success: 401 (Unauthorized)*' + } + else { + $exceptionMessage1 = '*The remote server returned an error: (401) Unauthorized*' + } + + $exceptionMessage2 = '*This endpoint requires you to be authenticated. | https://docs.github.com/v3/#authentication*' + + Mock -CommandName Get-AccessToken -ModuleName $script:moduleName + } + + It 'Should throw the correct exception' { + $invokeGHGraphQLParms = @{ + Body = $testBody + } + { Invoke-GHGraphQl @invokeGHGraphQlParms } | + Should -Throw + + $Error[0].Exception.Message | Should -BeLike $exceptionMessage1 + $Error[0].Exception.Message | Should -BeLike $exceptionMessage2 + $Error[0].Exception.Message | Should -BeLike '*RequestId:*' + $Error[0].CategoryInfo.Category | Should -Be 'InvalidOperation' + $Error[0].CategoryInfo.TargetName | Should -Be $testBody + $Error[0].FullyQualifiedErrorId | Should -BeLike '*Invoke-GHGraphQl' + } + } + } + + Context 'When there is an other exception in Invoke-WebRequest' { + BeforeAll { + $testWebRequestTimeoutSec = 'invalid' + $testBody = 'testBody' + + Mock -CommandName Get-GitHubConfiguration -ModuleName $script:moduleName ` + -ParameterFilter { $Name -eq 'WebRequestTimeoutSec' } ` + -MockWith { 'invalid' } + } + + It 'Should throw the correct exception' { + $invokeGHGraphQLParms = @{ + Body = $testBody + } + { Invoke-GHGraphQl @invokeGHGraphQlParms } | + Should -Throw "Cannot convert value ""$testWebRequestTimeoutSec""" + + $Error[0].CategoryInfo.Category | Should -Be 'InvalidArgument' + $Error[0].CategoryInfo.TargetName | Should -Be $testBody + $Error[0].FullyQualifiedErrorId | Should -BeLike 'CannotConvertArgumentNoMessage*' + } + } + + Context 'When the GraphQl JSON Query is Invalid' { + BeforeAll { + $invalidQuery = 'InvalidQuery' + $testBody = "{ ""query"":""$invalidQuery"" }" + } + + It 'Should throw the correct exception' { + $invokeGHGraphQLParms = @{ + Body = $testBody + } + { Invoke-GHGraphQl @invokeGHGraphQlParms } | Should -Throw + + $Error[0].Exception.Message | Should -BeLike "*Parse error on ""$invalidQuery""*" + $Error[0].Exception.Message | Should -BeLike '*RequestId:*' + $Error[0].CategoryInfo.Category | Should -Be 'NotSpecified' + $Error[0].CategoryInfo.TargetName | Should -Be $testBody + $Error[0].FullyQualifiedErrorId | Should -BeLike '*Invoke-GHGraphQl' + } + } + + Context 'When the GraphQl JSON query returns an error of ''NOT_FOUND''' { + BeforeAll { + $testOwner = 'microsoft' + $testRepo = 'nonexisting-repo' + $testQuery = "query repo { repository(name: \""$testRepo\"", owner: \""$testOwner\"") { id } }" + $testBody = "{ ""query"": ""$testQuery"" }" + } + + It 'Should throw the correct exception' { + $invokeGHGraphQLParms = @{ + Body = $testBody + } + { Invoke-GHGraphQl @invokeGHGraphQlParms } | Should -Throw + + $Error[0].Exception.Message | Should -BeLike "*Could not resolve to a Repository with the name '$testOwner/$testRepo'*" + $Error[0].Exception.Message | Should -BeLike '*RequestId:*' + $Error[0].CategoryInfo.Category | Should -Be 'ObjectNotFound' + $Error[0].CategoryInfo.TargetName | Should -Be $testBody + $Error[0].FullyQualifiedErrorId | Should -Be 'NOT_FOUND,Invoke-GHGraphQl' + } + } + } +} +finally +{ + if (Test-Path -Path $script:originalConfigFile -PathType Leaf) + { + # Restore the user's configuration to its pre-test state + Restore-GitHubConfiguration -Path $script:originalConfigFile + $script:originalConfigFile = $null + } +} From 3aa83dc0eebd2420377993e53372d2a230e737b1 Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Sun, 24 Jan 2021 10:56:06 +0000 Subject: [PATCH 09/33] Fix manifest module/function order --- PowerShellForGitHub.psd1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PowerShellForGitHub.psd1 b/PowerShellForGitHub.psd1 index 49ed0f35..74d5e489 100644 --- a/PowerShellForGitHub.psd1 +++ b/PowerShellForGitHub.psd1 @@ -34,11 +34,11 @@ 'GitHubAssignees.ps1', 'GitHubBranches.ps1', 'GitHubCore.ps1', - 'GitHubGraphQl.ps1', 'GitHubContents.ps1', 'GitHubEvents.ps1', 'GitHubGistComments.ps1', 'GitHubGists.ps1', + 'GitHubGraphQl.ps1', 'GitHubIssueComments.ps1', 'GitHubIssues.ps1', 'GitHubLabels.ps1', @@ -123,8 +123,8 @@ 'Group-GitHubIssue', 'Group-GitHubPullRequest', 'Initialize-GitHubLabel', - 'Invoke-GHRestMethod', 'Invoke-GHGraphQl', + 'Invoke-GHRestMethod', 'Invoke-GHRestMethodMultipleResult', 'Join-GitHubUri', 'Lock-GitHubIssue', From 855afe34065227985cb08c31a93f4c9f99f5f59f Mon Sep 17 00:00:00 2001 From: Simon Heather <32168619+X-Guardian@users.noreply.github.com> Date: Mon, 22 Mar 2021 08:14:52 +0000 Subject: [PATCH 10/33] Apply suggestions from code review Co-authored-by: Howard Wolosky --- GitHubBranches.ps1 | 18 +++++++++--------- GitHubGraphQl.ps1 | 22 ++++++++++++---------- Tests/GitHubGraphQl.Tests.ps1 | 22 ++++++++++++++-------- 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 index 3861199e..3f2d89ad 100644 --- a/GitHubBranches.ps1 +++ b/GitHubBranches.ps1 @@ -1185,7 +1185,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule PositionalBinding = $false, SupportsShouldProcess, DefaultParameterSetName = 'Elements')] - [OutputType({$script:GitHubBranchPatternProtectionRuleTypeName })] + [OutputType({$script:GitHubBranchPatternProtectionRuleTypeName})] param( [Parameter(ParameterSetName = 'Elements')] [string] $OwnerName, @@ -1352,7 +1352,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule $hashbody = @{query = "query organization { organization(login: ""$OrganizationName"") " + "{ team(slug: ""$team"") { id } } }"} - $description = "Querying $OrganizationName organisation for team $team" + $description = "Querying $OrganizationName organization for team $team" Write-Debug -Message $description @@ -1394,7 +1394,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule $teamPermission = Get-GitHubRepositoryTeamPermission @getGitHubRepositoryTeamPermissionParms - if ($teamPermission.permissions.push -eq $true -or $teamPermission.permissions.maintain -eq $true) + if (($teamPermission.permissions.push -eq $true) -or ($teamPermission.permissions.maintain -eq $true)) { $reviewDismissalActorIds += $result.data.organization.team.id } @@ -1461,7 +1461,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule if ($PSBoundParameters.ContainsKey('RestrictPushUsers')) { - foreach($user in $RestrictPushUsers) + foreach ($user in $RestrictPushUsers) { $hashbody = @{query = "query user { user(login: ""$user"") { id } }"} @@ -1497,7 +1497,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule $hashbody = @{query = "query organization { organization(login: ""$OrganizationName"") " + "{ team(slug: ""$team"") { id } } }"} - $description = "Querying $OrganizationName organisation for team $team" + $description = "Querying $OrganizationName organization for team $team" Write-Debug -Message $description @@ -2161,7 +2161,7 @@ filter Add-GitHubBranchPatternProtectionRuleAdditionalProperties $restrictPushApps = @() $restrictPushTeams = @() - $RestrictPushUsers = @() + $restrictPushUsers = @() foreach ($actor in $item.pushAllowances.nodes.actor) { @@ -2175,11 +2175,11 @@ filter Add-GitHubBranchPatternProtectionRuleAdditionalProperties } elseif ($actor.__typename -eq 'User') { - $RestrictPushUsers += $actor.login + $restrictPushUsers += $actor.login } else { - Write-Warning "Unknown restrict push actor type found $($actor.__typename). Ignoring" + Write-Log -Message "Unknown restrict push actor type found $($actor.__typename). Ignoring" -Level Warning } } @@ -2202,7 +2202,7 @@ filter Add-GitHubBranchPatternProtectionRuleAdditionalProperties } else { - Write-Warning "Unknown dismissal actor type found $($actor.__typename). Ignoring" + Write-Log -Message "Unknown dismissal actor type found $($actor.__typename). Ignoring" -Level Warning } } diff --git a/GitHubGraphQl.ps1 b/GitHubGraphQl.ps1 index c63c5582..09db6586 100644 --- a/GitHubGraphQl.ps1 +++ b/GitHubGraphQl.ps1 @@ -114,7 +114,7 @@ function Invoke-GHGraphQl Body = $bodyAsBytes UseDefaultCredentials = $true UseBasicParsing = $true - TimeoutSec = Get-GitHubConfiguration -Name WebRequestTimeoutSec + TimeoutSec = $timeOut } if (Get-GitHubConfiguration -Name LogRequestBody) @@ -175,7 +175,8 @@ function Invoke-GHGraphQl { Write-Debug -Message "Processing Error Details message '$errorDetailsMessage'" - try { + try + { Write-Debug -Message 'Checking Error Details message for JSON content' $errorDetailsMessageJson = $errorDetailsMessage | ConvertFrom-Json @@ -194,8 +195,8 @@ function Invoke-GHGraphQl Write-Debug -Message "Error Details Message: $($errorDetailsMessageJson.message)" Write-Debug -Message "Error Details Documentation URL: $($errorDetailsMessageJson.documentation_url)" - $errorMessage += ($($errorDetailsMessageJson.message.Trim()) + - " | $($errorDetailsMessageJson.documentation_url.Trim())") + $errorMessage += $errorDetailsMessageJson.message.Trim() + + ' | ' + $errorDetailsMessageJson.documentation_url.Trim() if ($errorDetailsMessageJson.details) { @@ -228,8 +229,8 @@ function Invoke-GHGraphQl } elseif ($ex.Response.PSTypeNames[0] -eq 'System.Net.HttpWebResponse') { - if ($ex.Response.Headers.Count -gt 0 -and - -not [System.String]::IsNullOrEmpty($ex.Response.Headers['X-GitHub-Request-Id'])) + if (($ex.Response.Headers.Count -gt 0) -and + (-not [System.String]::IsNullOrEmpty($ex.Response.Headers['X-GitHub-Request-Id']))) { $requestId = $ex.Response.Headers['X-GitHub-Request-Id'] } @@ -278,7 +279,8 @@ function Invoke-GHGraphQl $PSCmdlet.ThrowTerminatingError($errorRecord) } } - finally { + finally + { Write-Debug -Message "Processing Invoke-WebRequest 'finally' block" # Restore original security protocol @@ -302,18 +304,18 @@ function Invoke-GHGraphQl if (-not [System.String]::IsNullOrEmpty($graphQlResult.errors[0].type)) { $errorId = $graphQlResult.errors[0].type - switch ($graphQlResult.errors[0].type) + switch ($errorId) { 'NOT_FOUND' { - Write-Debug -Message "GraphQl Error Type: $($graphQlResult.errors[0].type)" + Write-Debug -Message "GraphQl Error Type: $errorId" $errorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound } Default { - Write-Debug -Message "GraphQL Unknown Error Type: $($graphQlResult.errors[0].type)" + Write-Debug -Message "GraphQL Unknown Error Type: $errorId" $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation } diff --git a/Tests/GitHubGraphQl.Tests.ps1 b/Tests/GitHubGraphQl.Tests.ps1 index 16e860c4..67b25549 100644 --- a/Tests/GitHubGraphQl.Tests.ps1 +++ b/Tests/GitHubGraphQl.Tests.ps1 @@ -20,8 +20,8 @@ try Describe 'GitHubCore/Invoke-GHGraphQl' { BeforeAll { $Description = 'description' - $AccessToken='' - $TelemetryEventName= $null + $AccessToken = '' + $TelemetryEventName = $null $TelemetryProperties = @{} $TelemetryExceptionBucket = $null @@ -54,12 +54,14 @@ try $testHostName = 'invalidhostname' $testBody = 'testBody' - if ($PSVersionTable.PSEdition -eq 'Core') { + if ($PSVersionTable.PSEdition -eq 'Core') + { $exceptionMessage = 'No such host is known' $categoryInfo = 'InvalidOperation' $targetName = "*$testHostName*" } - else { + else + { $exceptionMessage = "The remote name could not be resolved: '$testHostName'" $categoryInfo = 'NotSpecified' $targetName = $testBody @@ -88,10 +90,12 @@ try BeforeAll { $testBody = 'InvalidJson' - if ($PSVersionTable.PSEdition -eq 'Core') { + if ($PSVersionTable.PSEdition -eq 'Core') + { $exceptionMessage1 = '*Response status code does not indicate success: 400 (Bad Request)*' } - else { + else + { $exceptionMessage1 = '*The remote server returned an error: (400) Bad Request*' } @@ -118,10 +122,12 @@ try BeforeAll { $testBody = '{ "query": "query login { viewer { login } }" }' - if ($PSVersionTable.PSEdition -eq 'Core') { + if ($PSVersionTable.PSEdition -eq 'Core') + { $exceptionMessage1 = '*Response status code does not indicate success: 401 (Unauthorized)*' } - else { + else + { $exceptionMessage1 = '*The remote server returned an error: (401) Unauthorized*' } From 7b4f0cf797553f3c5a71c1f117e99d00456bb4eb Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Mon, 22 Mar 2021 18:33:32 +0000 Subject: [PATCH 11/33] Formatting and code review fixes --- GitHubBranches.ps1 | 53 +++++++++++++++++++++++++++------------------- GitHubGraphQl.ps1 | 18 +++++++++------- 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 index 3f2d89ad..7bc66d38 100644 --- a/GitHubBranches.ps1 +++ b/GitHubBranches.ps1 @@ -1185,7 +1185,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule PositionalBinding = $false, SupportsShouldProcess, DefaultParameterSetName = 'Elements')] - [OutputType({$script:GitHubBranchPatternProtectionRuleTypeName})] + [OutputType( { $script:GitHubBranchPatternProtectionRuleTypeName })] param( [Parameter(ParameterSetName = 'Elements')] [string] $OwnerName, @@ -1258,7 +1258,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule RepositoryName = (Get-PiiSafeString -PlainText $RepositoryName) } - $hashbody = @{query = "query repo { repository(name: ""$RepositoryName"", owner: ""$OwnerName"") { id } }"} + $hashbody = @{query = "query repo { repository(name: ""$RepositoryName"", owner: ""$OwnerName"") { id } }" } Write-Debug -Message "Querying Repository $RepositoryName, Owner $OwnerName" @@ -1310,13 +1310,13 @@ filter New-GitHubRepositoryBranchPatternProtectionRule } if ($PSBoundParameters.ContainsKey('DismissalUsers') -or - $PSBoundParameters.ContainsKey('DismissalTeams')) + $PSBoundParameters.ContainsKey('DismissalTeams')) { $reviewDismissalActorIds = @() if ($PSBoundParameters.ContainsKey('DismissalUsers')) { - foreach($user in $DismissalUsers) + foreach ($user in $DismissalUsers) { $hashbody = @{query = "query user { user(login: ""$user"") { id } }"} @@ -1347,10 +1347,11 @@ filter New-GitHubRepositoryBranchPatternProtectionRule if ($PSBoundParameters.ContainsKey('DismissalTeams')) { - foreach($team in $DismissalTeams) + foreach ($team in $DismissalTeams) { $hashbody = @{query = "query organization { organization(login: ""$OrganizationName"") " + - "{ team(slug: ""$team"") { id } } }"} + "{ team(slug: ""$team"") { id } } }" + } $description = "Querying $OrganizationName organization for team $team" @@ -1375,8 +1376,12 @@ filter New-GitHubRepositoryBranchPatternProtectionRule if ([System.String]::IsNullOrEmpty($result.data.organization.team)) { + $errorMessage = "Team $team not found in organization $OrganizationName" + + Write-Log -Level Error -Message ($errorMessage) + $newErrorRecordParms = @{ - ErrorMessage = "Team $team not found in organization $OrganizationName" + ErrorMessage = $errorMessage ErrorId = 'DismissalTeamNotFound' ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound TargetObject = $team @@ -1463,7 +1468,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule { foreach ($user in $RestrictPushUsers) { - $hashbody = @{query = "query user { user(login: ""$user"") { id } }"} + $hashbody = @{query = "query user { user(login: ""$user"") { id } }" } $description = "Querying User $user" @@ -1492,10 +1497,11 @@ filter New-GitHubRepositoryBranchPatternProtectionRule if ($PSBoundParameters.ContainsKey('RestrictPushTeams')) { - foreach($team in $RestrictPushTeams) + foreach ($team in $RestrictPushTeams) { $hashbody = @{query = "query organization { organization(login: ""$OrganizationName"") " + - "{ team(slug: ""$team"") { id } } }"} + "{ team(slug: ""$team"") { id } } }" + } $description = "Querying $OrganizationName organization for team $team" @@ -1564,7 +1570,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule { foreach ($app in $RestrictPushApps) { - $hashbody = @{query = "query app { marketplaceListing(slug: ""$app"") { app { id } } }"} + $hashbody = @{query = "query app { marketplaceListing(slug: ""$app"") { app { id } } }" } $description = "Querying for app $app" @@ -1620,9 +1626,10 @@ filter New-GitHubRepositoryBranchPatternProtectionRule $mutationList += 'allowsDeletions: ' + $AllowDeletions.ToBool().ToString().ToLower() } - $mutationInput = $mutationList -join(',') + $mutationInput = $mutationList -join (',') $hashbody = @{query = "mutation ProtectionRule { createBranchProtectionRule(input: { $mutationInput }) " + - "{ clientMutationId } } " } + "{ clientMutationId } } " + } $description = "Setting $BranchPatternName branch protection status for $RepositoryName" $body = ConvertTo-Json -InputObject $hashBody @@ -1631,8 +1638,8 @@ filter New-GitHubRepositoryBranchPatternProtectionRule Write-Debug -Message "Query: $body" if (-not $PSCmdlet.ShouldProcess( - "Owner '$OwnerName', Repository '$RepositoryName'", - "Create '$BranchPatternName' branch pattern GitHub Repository Branch Protection Rule")) + "Owner '$OwnerName', Repository '$RepositoryName'", + "Create '$BranchPatternName' branch pattern GitHub Repository Branch Protection Rule")) { return } @@ -1715,9 +1722,9 @@ filter Get-GitHubRepositoryBranchPatternProtectionRule [CmdletBinding( PositionalBinding = $false, DefaultParameterSetName = 'Elements')] - [OutputType({ $script:GitHubBranchProtectionRuleTypeName })] + [OutputType( { $script:GitHubBranchProtectionRuleTypeName })] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "", - Justification="The Uri parameter is only referenced by Resolve-RepositoryElements which get access to it from the stack via Get-Variable -Scope 1.")] + Justification = "The Uri parameter is only referenced by Resolve-RepositoryElements which get access to it from the stack via Get-Variable -Scope 1.")] param( [Parameter(ParameterSetName = 'Elements')] [string] $OwnerName, @@ -1752,7 +1759,7 @@ filter Get-GitHubRepositoryBranchPatternProtectionRule 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) } - $branchProtectionRuleFields = 'allowsDeletions allowsForcePushes dismissesStaleReviews id ' + + $branchProtectionRuleFields = ('allowsDeletions allowsForcePushes dismissesStaleReviews id ' + 'isAdminEnforced pattern requiredApprovingReviewCount requiredStatusCheckContexts ' + 'requiresApprovingReviews requiresCodeOwnerReviews requiresCommitSignatures requiresLinearHistory ' + 'requiresStatusChecks requiresStrictStatusChecks restrictsPushes restrictsReviewDismissals ' + @@ -1760,7 +1767,7 @@ filter Get-GitHubRepositoryBranchPatternProtectionRule '... on Team { __typename name } ... on User { __typename login } } } }' + 'reviewDismissalAllowances(first: 100) { nodes { actor { ... on Team { __typename name } ' + '... on User { __typename login } } } } ' + - 'repository { url }' + 'repository { url }') $hashbody = @{query = "query branchProtectionRule { repository(name: ""$RepositoryName"", " + "owner: ""$OwnerName"") { branchProtectionRules(first: 100) { nodes { " + @@ -1869,7 +1876,7 @@ filter Remove-GitHubRepositoryBranchPatternProtectionRule DefaultParameterSetName = 'Elements', ConfirmImpact = "High")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "", - Justification="The Uri parameter is only referenced by Resolve-RepositoryElements which get access to it from the stack via Get-Variable -Scope 1.")] + Justification = "The Uri parameter is only referenced by Resolve-RepositoryElements which get access to it from the stack via Get-Variable -Scope 1.")] [Alias('Delete-GitHubRepositoryBranchPatternProtectionRule')] param( [Parameter(ParameterSetName = 'Elements')] @@ -1908,7 +1915,8 @@ filter Remove-GitHubRepositoryBranchPatternProtectionRule } $hashbody = @{query = "query branchProtectionRule { repository(name: ""$RepositoryName"", " + - "owner: ""$OwnerName"") { branchProtectionRules(first: 100) { nodes { id pattern } } } }"} + "owner: ""$OwnerName"") { branchProtectionRules(first: 100) { nodes { id pattern } } } }" + } $description = "Querying $RepositoryName repository for branch protection rules" @@ -1956,7 +1964,8 @@ filter Remove-GitHubRepositoryBranchPatternProtectionRule } $hashbody = @{query = "mutation ProtectionRule { deleteBranchProtectionRule(input: " + - "{ branchProtectionRuleId: ""$ruleId"" } ) { clientMutationId } }" } + "{ branchProtectionRuleId: ""$ruleId"" } ) { clientMutationId } }" + } $description = "Removing $BranchPatternName branch protection rule for $RepositoryName" $body = ConvertTo-Json -InputObject $hashBody diff --git a/GitHubGraphQl.ps1 b/GitHubGraphQl.ps1 index 09db6586..e8e05552 100644 --- a/GitHubGraphQl.ps1 +++ b/GitHubGraphQl.ps1 @@ -1,6 +1,6 @@ function Invoke-GHGraphQl { -<# + <# .SYNOPSIS A wrapper around Invoke-WebRequest that understands the GitHub GraphQL API. @@ -101,15 +101,16 @@ function Invoke-GHGraphQl } $timeOut = Get-GitHubConfiguration -Name WebRequestTimeoutSec + $method = 'Post' Write-Log -Message $Description -Level Verbose - Write-Log -Message "Accessing [$Method] $url [Timeout = $timeOut]" -Level Verbose + Write-Log -Message "Accessing [$method] $url [Timeout = $timeOut]" -Level Verbose $bodyAsBytes = [System.Text.Encoding]::UTF8.GetBytes($Body) $params = @{ Uri = $url - Method = 'Post' + Method = $method Headers = $headers Body = $bodyAsBytes UseDefaultCredentials = $true @@ -129,9 +130,10 @@ function Invoke-GHGraphQl $originalSecurityProtocol = [Net.ServicePointManager]::SecurityProtocol # Enforce TLS v1.2 Security Protocol - [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12 + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - try { + try + { $result = Invoke-WebRequest @params } catch @@ -175,7 +177,7 @@ function Invoke-GHGraphQl { Write-Debug -Message "Processing Error Details message '$errorDetailsMessage'" - try + try { Write-Debug -Message 'Checking Error Details message for JSON content' @@ -195,8 +197,8 @@ function Invoke-GHGraphQl Write-Debug -Message "Error Details Message: $($errorDetailsMessageJson.message)" Write-Debug -Message "Error Details Documentation URL: $($errorDetailsMessageJson.documentation_url)" - $errorMessage += $errorDetailsMessageJson.message.Trim() + - ' | ' + $errorDetailsMessageJson.documentation_url.Trim() + $errorMessage += ($errorDetailsMessageJson.message.Trim() + + ' | ' + $errorDetailsMessageJson.documentation_url.Trim()) if ($errorDetailsMessageJson.details) { From adb8af5457dfb4f78b4eb5bfd3d8c110abd0a77c Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Mon, 22 Mar 2021 18:43:43 +0000 Subject: [PATCH 12/33] Fix TelemetryEventNames --- GitHubBranches.ps1 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 index 7bc66d38..2b768d4e 100644 --- a/GitHubBranches.ps1 +++ b/GitHubBranches.ps1 @@ -1266,7 +1266,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule Body = ConvertTo-Json -InputObject $hashBody Description = "Querying $RepositoryName" AccessToken = $AccessToken - TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryEventName = 'Get-GitHubRepositoryQ1' TelemetryProperties = $telemetryProperties } @@ -1328,7 +1328,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule Body = ConvertTo-Json -InputObject $hashBody Description = $description AccessToken = $AccessToken - TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryEventName = 'Get-GitHubUserQ1' TelemetryProperties = $telemetryProperties } @@ -1361,7 +1361,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule Body = ConvertTo-Json -InputObject $hashBody Description = $description AccessToken = $AccessToken - TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryEventName = 'GetGitHubTeamQ1' TelemetryProperties = $telemetryProperties } @@ -1478,7 +1478,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule Body = ConvertTo-Json -InputObject $hashBody Description = $description AccessToken = $AccessToken - TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryEventName = 'GetGitHubUserQ1' TelemetryProperties = $telemetryProperties } @@ -1511,7 +1511,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule Body = ConvertTo-Json -InputObject $hashBody Description = $description AccessToken = $AccessToken - TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryEventName = 'GetGitHubTeamQ1' TelemetryProperties = $telemetryProperties } @@ -1580,7 +1580,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule Body = ConvertTo-Json -InputObject $hashBody Description = $description AccessToken = $AccessToken - TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryEventName = 'Get-GitHubAppQ1' TelemetryProperties = $telemetryProperties } @@ -1926,7 +1926,7 @@ filter Remove-GitHubRepositoryBranchPatternProtectionRule Body = ConvertTo-Json -InputObject $hashBody Description = $description AccessToken = $AccessToken - TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryEventName = 'Get-GitHubRepositoryQ1' TelemetryProperties = $telemetryProperties } From c0426050019e1950ccd94b658ad5476c71f6f375 Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Tue, 23 Mar 2021 17:58:45 +0000 Subject: [PATCH 13/33] Fix exception handling. --- GitHubBranches.ps1 | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 index 2b768d4e..49cb52e1 100644 --- a/GitHubBranches.ps1 +++ b/GitHubBranches.ps1 @@ -1652,11 +1652,13 @@ filter New-GitHubRepositoryBranchPatternProtectionRule TelemetryProperties = $telemetryProperties } - $result = Invoke-GHGraphQl @params - - if ($result -is [System.Management.Automation.ErrorRecord]) + try { - $PSCmdlet.ThrowTerminatingError($result) + $result = Invoke-GHGraphQl @params + } + catch + { + $PSCmdlet.ThrowTerminatingError($_) } } From 9d9bd71c1799fc4c222b3f23e89c9470d6583411 Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Tue, 23 Mar 2021 17:59:21 +0000 Subject: [PATCH 14/33] Add ValidateNotNullOrEmpty to StatusChecks --- GitHubBranches.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 index 49cb52e1..5e52119c 100644 --- a/GitHubBranches.ps1 +++ b/GitHubBranches.ps1 @@ -1208,6 +1208,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule Position = 2)] [string] $BranchPatternName, + [ValidateNotNullOrEmpty()] [string[]] $StatusChecks, [switch] $RequireStrictStatusChecks, From a0ec66c106543841816c1290738046e8263da690 Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Tue, 23 Mar 2021 18:24:23 +0000 Subject: [PATCH 15/33] Fixes for code review --- GitHubBranches.ps1 | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 index 5e52119c..4f42ad53 100644 --- a/GitHubBranches.ps1 +++ b/GitHubBranches.ps1 @@ -5,6 +5,9 @@ GitHubBranchTypeName = 'GitHub.Branch' GitHubBranchProtectionRuleTypeName = 'GitHub.BranchProtectionRule' GitHubBranchPatternProtectionRuleTypeName = 'GitHub.BranchPatternProtectionRule' + MaxProtectionRules = 100 + MaxPushAllowances = 100 + MaxReviewDismissalAllowances = 100 }.GetEnumerator() | ForEach-Object { Set-Variable -Scope Script -Option ReadOnly -Name $_.Key -Value $_.Value } @@ -1766,14 +1769,14 @@ filter Get-GitHubRepositoryBranchPatternProtectionRule 'isAdminEnforced pattern requiredApprovingReviewCount requiredStatusCheckContexts ' + 'requiresApprovingReviews requiresCodeOwnerReviews requiresCommitSignatures requiresLinearHistory ' + 'requiresStatusChecks requiresStrictStatusChecks restrictsPushes restrictsReviewDismissals ' + - 'pushAllowances(first: 100) { nodes { actor { ... on App { __typename name } ' + + "pushAllowances(first: $script:MaxPushAllowances) { nodes { actor { ... on App { __typename name } " + '... on Team { __typename name } ... on User { __typename login } } } }' + - 'reviewDismissalAllowances(first: 100) { nodes { actor { ... on Team { __typename name } ' + - '... on User { __typename login } } } } ' + + "reviewDismissalAllowances(first: $script:MaxReviewDismissalAllowances)" + + '{ nodes { actor { ... on Team { __typename name } ... on User { __typename login } } } } ' + 'repository { url }') $hashbody = @{query = "query branchProtectionRule { repository(name: ""$RepositoryName"", " + - "owner: ""$OwnerName"") { branchProtectionRules(first: 100) { nodes { " + + "owner: ""$OwnerName"") { branchProtectionRules(first: $script:MaxProtectionRules) { nodes { " + "$branchProtectionRuleFields } } } }"} $description = "Querying $RepositoryName repository for branch protection rules" @@ -1918,7 +1921,7 @@ filter Remove-GitHubRepositoryBranchPatternProtectionRule } $hashbody = @{query = "query branchProtectionRule { repository(name: ""$RepositoryName"", " + - "owner: ""$OwnerName"") { branchProtectionRules(first: 100) { nodes { id pattern } } } }" + "owner: ""$OwnerName"") { branchProtectionRules(first: $script:MaxProtectionRules) { nodes { id pattern } } } }" } $description = "Querying $RepositoryName repository for branch protection rules" From 30e75a63c09f021af1e089f1d456256d287dc18c Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Sat, 27 Mar 2021 10:21:16 +0000 Subject: [PATCH 16/33] Fixes for code review --- GitHubBranches.ps1 | 86 +++++++++++++++++++--------------- GitHubGraphQl.ps1 | 14 +++--- Tests/GitHubBranches.tests.ps1 | 34 +++++++------- 3 files changed, 72 insertions(+), 62 deletions(-) diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 index 4f42ad53..b17c586d 100644 --- a/GitHubBranches.ps1 +++ b/GitHubBranches.ps1 @@ -1107,7 +1107,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule .PARAMETER BranchPatternName The branch name pattern to create the protection rule on. - .PARAMETER StatusChecks + .PARAMETER StatusCheck The list of status checks to require in order to merge into the branch. .PARAMETER RequireStrictStatusChecks @@ -1117,10 +1117,10 @@ filter New-GitHubRepositoryBranchPatternProtectionRule .PARAMETER IsAdminEnforced Enforce all configured restrictions for administrators. - .PARAMETER DismissalUsers + .PARAMETER DismissalUser Specify the user names of users who can dismiss pull request reviews. - .PARAMETER DismissalTeams + .PARAMETER DismissalTeam Specify which teams can dismiss pull request reviews. This can only be specified for organization-owned repositories. @@ -1135,13 +1135,13 @@ filter New-GitHubRepositoryBranchPatternProtectionRule Specify the number of reviewers required to approve pull requests. Use a number between 1 and 6. - .PARAMETER RestrictPushUsers + .PARAMETER RestrictPushUser Specify which users have push access. - .PARAMETER RestrictPushTeams + .PARAMETER RestrictPushTeam Specify which teams have push access. - .PARAMETER RestrictPushApps + .PARAMETER RestrictPushApp Specify which apps have push access. .PARAMETER RequireLinearHistory @@ -1212,15 +1212,17 @@ filter New-GitHubRepositoryBranchPatternProtectionRule [string] $BranchPatternName, [ValidateNotNullOrEmpty()] - [string[]] $StatusChecks, + [string[]] $StatusCheck, [switch] $RequireStrictStatusChecks, [switch] $IsAdminEnforced, - [string[]] $DismissalUsers, + [ValidateNotNullOrEmpty()] + [string[]] $DismissalUser, - [string[]] $DismissalTeams, + [ValidateNotNullOrEmpty()] + [string[]] $DismissalTeam, [switch] $DismissStaleReviews, @@ -1229,11 +1231,14 @@ filter New-GitHubRepositoryBranchPatternProtectionRule [ValidateRange(1, 6)] [int] $RequiredApprovingReviewCount, - [string[]] $RestrictPushUsers, + [ValidateNotNullOrEmpty()] + [string[]] $RestrictPushUser, - [string[]] $RestrictPushTeams, + [ValidateNotNullOrEmpty()] + [string[]] $RestrictPushTeam, - [string[]] $RestrictPushApps, + [ValidateNotNullOrEmpty()] + [string[]] $RestrictPushApp, [switch] $RequireLinearHistory, @@ -1293,8 +1298,8 @@ filter New-GitHubRepositoryBranchPatternProtectionRule if ($PSBoundParameters.ContainsKey('RequiredApprovingReviewCount') -or $PSBoundParameters.ContainsKey('DismissStaleReviews') -or $PSBoundParameters.ContainsKey('RequireCodeOwnerReviews') -or - $PSBoundParameters.ContainsKey('DismissalUsers') -or - $PSBoundParameters.ContainsKey('DismissalTeams')) + $PSBoundParameters.ContainsKey('DismissalUser') -or + $PSBoundParameters.ContainsKey('DismissalTeam')) { $mutationList += 'requiresApprovingReviews: true' @@ -1313,14 +1318,14 @@ filter New-GitHubRepositoryBranchPatternProtectionRule $mutationList += 'requiresCodeOwnerReviews: ' + $RequireCodeOwnerReviews.ToBool().ToString().ToLower() } - if ($PSBoundParameters.ContainsKey('DismissalUsers') -or - $PSBoundParameters.ContainsKey('DismissalTeams')) + if ($PSBoundParameters.ContainsKey('DismissalUser') -or + $PSBoundParameters.ContainsKey('DismissalTeam')) { $reviewDismissalActorIds = @() - if ($PSBoundParameters.ContainsKey('DismissalUsers')) + if ($PSBoundParameters.ContainsKey('DismissalUser')) { - foreach ($user in $DismissalUsers) + foreach ($user in $DismissalUser) { $hashbody = @{query = "query user { user(login: ""$user"") { id } }"} @@ -1349,9 +1354,9 @@ filter New-GitHubRepositoryBranchPatternProtectionRule } } - if ($PSBoundParameters.ContainsKey('DismissalTeams')) + if ($PSBoundParameters.ContainsKey('DismissalTeam')) { - foreach ($team in $DismissalTeams) + foreach ($team in $DismissalTeam) { $hashbody = @{query = "query organization { organization(login: ""$OrganizationName"") " + "{ team(slug: ""$team"") { id } } }" @@ -1430,7 +1435,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule } # Process 'Require status checks to pass before merging' properties - if ($PSBoundParameters.ContainsKey('StatusChecks') -or + if ($PSBoundParameters.ContainsKey('StatusCheck') -or $PSBoundParameters.ContainsKey('RequireStrictStatusChecks')) { $mutationList += 'requiresStatusChecks: true' @@ -1440,9 +1445,9 @@ filter New-GitHubRepositoryBranchPatternProtectionRule $mutationList += 'requiresStrictStatusChecks: ' + $RequireStrictStatusChecks.ToBool().ToString().ToLower() } - if ($PSBoundParameters.ContainsKey('StatusChecks')) + if ($PSBoundParameters.ContainsKey('StatusCheck')) { - $mutationList += 'requiredStatusCheckContexts: [ "' + ($StatusChecks -join ('","')) + '" ]' + $mutationList += 'requiredStatusCheckContexts: [ "' + ($StatusCheck -join ('","')) + '" ]' } } @@ -1462,15 +1467,15 @@ filter New-GitHubRepositoryBranchPatternProtectionRule } # Process 'Restrict who can push to matching branches' properties - if ($PSBoundParameters.ContainsKey('RestrictPushUsers') -or - $PSBoundParameters.ContainsKey('RestrictPushTeams') -or - $PSBoundParameters.ContainsKey('RestrictPushApps')) + if ($PSBoundParameters.ContainsKey('RestrictPushUser') -or + $PSBoundParameters.ContainsKey('RestrictPushTeam') -or + $PSBoundParameters.ContainsKey('RestrictPushApp')) { $restrictPushActorIds = @() - if ($PSBoundParameters.ContainsKey('RestrictPushUsers')) + if ($PSBoundParameters.ContainsKey('RestrictPushUser')) { - foreach ($user in $RestrictPushUsers) + foreach ($user in $RestrictPushUser) { $hashbody = @{query = "query user { user(login: ""$user"") { id } }" } @@ -1499,9 +1504,9 @@ filter New-GitHubRepositoryBranchPatternProtectionRule } } - if ($PSBoundParameters.ContainsKey('RestrictPushTeams')) + if ($PSBoundParameters.ContainsKey('RestrictPushTeam')) { - foreach ($team in $RestrictPushTeams) + foreach ($team in $RestrictPushTeam) { $hashbody = @{query = "query organization { organization(login: ""$OrganizationName"") " + "{ team(slug: ""$team"") { id } } }" @@ -1570,9 +1575,9 @@ filter New-GitHubRepositoryBranchPatternProtectionRule } } - if ($PSBoundParameters.ContainsKey('RestrictPushApps')) + if ($PSBoundParameters.ContainsKey('RestrictPushApp')) { - foreach ($app in $RestrictPushApps) + foreach ($app in $RestrictPushApp) { $hashbody = @{query = "query app { marketplaceListing(slug: ""$app"") { app { id } } }" } @@ -1746,9 +1751,7 @@ filter Get-GitHubRepositoryBranchPatternProtectionRule [Alias('RepositoryUrl')] [string] $Uri, - [Parameter( - Mandatory, - Position = 2)] + [Parameter(Position = 2)] [string] $BranchPatternName, [string] $AccessToken @@ -1803,11 +1806,18 @@ filter Get-GitHubRepositoryBranchPatternProtectionRule if ($result.data.repository.branchProtectionRules) { - $rule = ($result.data.repository.branchProtectionRules.nodes | - Where-Object -Property pattern -eq $BranchPatternName) + if ($PSBoundParameters.ContainsKey('BranchPatternName')) + { + $rule = ($result.data.repository.branchProtectionRules.nodes | + Where-Object -Property pattern -eq $BranchPatternName) + } + else + { + $rule = $result.data.repository.branchProtectionRules.nodes + } } - if (!$rule) + if (!$rule -and $PSBoundParameters.ContainsKey('BranchPatternName')) { $newErrorRecordParms = @{ ErrorMessage = "Branch Protection Rule '$BranchPatternName' not found on repository $RepositoryName" diff --git a/GitHubGraphQl.ps1 b/GitHubGraphQl.ps1 index e8e05552..29bd9f8c 100644 --- a/GitHubGraphQl.ps1 +++ b/GitHubGraphQl.ps1 @@ -287,14 +287,14 @@ function Invoke-GHGraphQl # Restore original security protocol [Net.ServicePointManager]::SecurityProtocol = $originalSecurityProtocol + } - # Record the telemetry for this event. - $stopwatch.Stop() - if (-not [String]::IsNullOrEmpty($TelemetryEventName)) - { - $telemetryMetrics = @{ 'Duration' = $stopwatch.Elapsed.TotalSeconds } - Set-TelemetryEvent -EventName $TelemetryEventName -Properties $localTelemetryProperties -Metrics $telemetryMetrics - } + # Record the telemetry for this event. + $stopwatch.Stop() + if (-not [String]::IsNullOrEmpty($TelemetryEventName)) + { + $telemetryMetrics = @{ 'Duration' = $stopwatch.Elapsed.TotalSeconds } + Set-TelemetryEvent -EventName $TelemetryEventName -Properties $localTelemetryProperties -Metrics $telemetryMetrics } $graphQlResult = $result.Content | ConvertFrom-Json diff --git a/Tests/GitHubBranches.tests.ps1 b/Tests/GitHubBranches.tests.ps1 index 5b2d543f..2ed2b4a1 100644 --- a/Tests/GitHubBranches.tests.ps1 +++ b/Tests/GitHubBranches.tests.ps1 @@ -611,7 +611,7 @@ try $newGitHubRepositoryBranchProtectionParms = @{ Uri = $repo.svn_url BranchName = $targetBranchName - RestrictPushUsers = $script:OwnerName + RestrictPushUser = $script:OwnerName } $rule = New-GitHubRepositoryBranchProtectionRule @newGitHubRepositoryBranchProtectionParms @@ -853,8 +853,8 @@ try RequireCommitSignatures = $true RequireLinearHistory = $true IsAdminEnforced = $true - RestrictPushUsers = $script:OwnerName - RestrictPushTeams = $TeamName + RestrictPushUser = $script:OwnerName + RestrictPushTeam = $TeamName AllowForcePushes = $true AllowDeletions = $true } @@ -901,8 +901,8 @@ try RequiredApprovingReviewCount = 1 DismissStaleReviews = $true RequireCodeOwnerReviews = $true - DismissalUsers = $script:OwnerName - DismissalTeams = $teamName + DismissalUser = $script:OwnerName + DismissalTeam = $teamName } New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms @@ -935,7 +935,7 @@ try Uri = $repo.svn_url BranchPatternName = $branchPatternName RequireStrictStatusChecks = $true - StatusChecks = $statusChecks + StatusCheck = $statusChecks } New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms @@ -1072,8 +1072,8 @@ try RequireCommitSignatures = $true RequireLinearHistory = $true IsAdminEnforced = $true - RestrictPushUsers = $script:OwnerName - RestrictPushTeams = $pushTeamName + RestrictPushUser = $script:OwnerName + RestrictPushTeam = $pushTeamName AllowForcePushes = $true AllowDeletions = $true } @@ -1121,7 +1121,7 @@ try $newGitHubRepositoryBranchPatternProtectionParms = @{ Uri = $repo.svn_url BranchPatternName = $branchPatternName - RestrictPushTeams = $mockTeamName + RestrictPushTeam = $mockTeamName } } @@ -1139,7 +1139,7 @@ try $newGitHubRepositoryBranchPatternProtectionParms = @{ Uri = $repo.svn_url BranchPatternName = $branchPatternName - RestrictPushTeams = $pullTeamName + RestrictPushTeam = $pullTeamName } } @@ -1161,8 +1161,8 @@ try RequiredApprovingReviewCount = 1 DismissStaleReviews = $true RequireCodeOwnerReviews = $true - DismissalUsers = $script:OwnerName - DismissalTeams = $pushTeamName + DismissalUser = $script:OwnerName + DismissalTeam = $pushTeamName } } @@ -1195,7 +1195,7 @@ try $newGitHubRepositoryBranchPatternProtectionParms = @{ Uri = $repo.svn_url BranchPatternName = $branchPatternName - DismissalTeams = $mockTeamName + DismissalTeam = $mockTeamName } } @@ -1213,7 +1213,7 @@ try $newGitHubRepositoryBranchPatternProtectionParms = @{ Uri = $repo.svn_url BranchPatternName = $branchPatternName - DismissalTeams = $pullTeamName + DismissalTeam = $pullTeamName } } @@ -1228,13 +1228,13 @@ try Context 'When setting required status checks' { BeforeAll { $branchPatternName = [Guid]::NewGuid().Guid + '/**/*' - $statusChecks = 'test' + $statusCheck = 'test' $newGitHubRepositoryBranchPatternProtectionParms = @{ Uri = $repo.svn_url BranchPatternName = $branchPatternName RequireStrictStatusChecks = $true - StatusChecks = $statusChecks + StatusCheck = $statusCheck } } @@ -1251,7 +1251,7 @@ try $rule.RepositoryUrl | Should -Be $repo.RepositoryUrl $rule.requiresStatusChecks | Should -BeTrue $rule.requiresStrictStatusChecks | Should -BeTrue - $rule.requiredStatusCheckContexts | Should -Contain $statusChecks + $rule.requiredStatusCheckContexts | Should -Contain $statusCheck } } From 5421771fb2f86dd7ea05f32fcb5719be82c06767 Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Sat, 27 Mar 2021 10:21:46 +0000 Subject: [PATCH 17/33] Fix usage.md --- USAGE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/USAGE.md b/USAGE.md index 7c0c4002..82a076e3 100644 --- a/USAGE.md +++ b/USAGE.md @@ -709,19 +709,19 @@ Remove-GitHubRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName #### Getting a repository branch pattern protection rule ```powershell -Get-GitHubRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchPatternName 'Release/**/*' +Get-GitHubRepositoryBranchPatternProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchPatternName 'Release/**/*' ``` #### Creating a repository branch pattern protection rule ```powershell -New-GitHubRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchPatternName 'Release/**/*' -RequiredApprovingReviewCount 1 -DismissStaleReviews -RequireStrictStatusChecks -StatusChecks 'CICheck' +New-GitHubRepositoryBranchPatternProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchPatternName 'Release/**/*' -RequiredApprovingReviewCount 1 -DismissStaleReviews -RequireStrictStatusChecks -StatusCheck 'CICheck' ``` #### Removing a repository branch pattern protection rule ```powershell -Remove-GitHubRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchPatternName 'Release/**/*' +Remove-GitHubRepositoryBranchPatternProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchPatternName 'Release/**/*' ``` ---------- From f2e85a77a88342516fcdace9ea1bbe25a895deec Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Sat, 27 Mar 2021 12:35:48 +0000 Subject: [PATCH 18/33] Refactor Team hanmdling --- GitHubBranches.ps1 | 126 +++++++++++++++++++++------------------------ 1 file changed, 58 insertions(+), 68 deletions(-) diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 index b17c586d..8f64cb22 100644 --- a/GitHubBranches.ps1 +++ b/GitHubBranches.ps1 @@ -1294,6 +1294,21 @@ filter New-GitHubRepositoryBranchPatternProtectionRule "repositoryId: ""$repoId"", pattern: ""$BranchPatternName""" ) + if ($PSBoundParameters.ContainsKey('DismissalTeam') -or + $PSBoundParameters.ContainsKey('RestrictPushTeam')) + { + Write-Debug -Message "Getting details for all GitHub Teams in Organization '$OrganizationName'" + + try + { + $orgTeams = Get-GitHubTeam -OrganizationName $OrganizationName + } + catch + { + $PSCmdlet.ThrowTerminatingError($_) + } + } + # Process 'Require pull request reviews before merging' properties if ($PSBoundParameters.ContainsKey('RequiredApprovingReviewCount') -or $PSBoundParameters.ContainsKey('DismissStaleReviews') -or @@ -1358,45 +1373,21 @@ filter New-GitHubRepositoryBranchPatternProtectionRule { foreach ($team in $DismissalTeam) { - $hashbody = @{query = "query organization { organization(login: ""$OrganizationName"") " + - "{ team(slug: ""$team"") { id } } }" - } - - $description = "Querying $OrganizationName organization for team $team" - - Write-Debug -Message $description - - $params = @{ - Body = ConvertTo-Json -InputObject $hashBody - Description = $description - AccessToken = $AccessToken - TelemetryEventName = 'GetGitHubTeamQ1' - TelemetryProperties = $telemetryProperties - } + $teamDetail = $orgTeams | Where-Object -Property Name -eq $RestrictPushTeam - try - { - $result = Invoke-GHGraphQl @params - } - catch + if ($teamDetail.Count -eq 0) { - $PSCmdlet.ThrowTerminatingError($_) - } - - if ([System.String]::IsNullOrEmpty($result.data.organization.team)) - { - $errorMessage = "Team $team not found in organization $OrganizationName" - - Write-Log -Level Error -Message ($errorMessage) - $newErrorRecordParms = @{ - ErrorMessage = $errorMessage + ErrorMessage = "Team '$team' not found in organization '$OrganizationName'" ErrorId = 'DismissalTeamNotFound' ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound TargetObject = $team } $errorRecord = New-ErrorRecord @newErrorRecordParms + Write-Log -Exception $errorRecor -Level Error + Set-TelemetryException -Exception $ex -ErrorBucket $errorBucket -Properties $localTelemetryProperties + $PSCmdlet.ThrowTerminatingError($errorRecord) } @@ -1406,26 +1397,36 @@ filter New-GitHubRepositoryBranchPatternProtectionRule RepositoryName = $repositoryName } - $teamPermission = Get-GitHubRepositoryTeamPermission @getGitHubRepositoryTeamPermissionParms + Write-Debug -Message "Getting GitHub Permissions for Team '$team' on Repository '$RepositoryName'" + + try + { + $teamPermission = Get-GitHubRepositoryTeamPermission @getGitHubRepositoryTeamPermissionParms + } + catch + { + Write-Debug -Message "Team '$team' has no permissions on Repository '$RepositoryName'" + } if (($teamPermission.permissions.push -eq $true) -or ($teamPermission.permissions.maintain -eq $true)) { - $reviewDismissalActorIds += $result.data.organization.team.id + $reviewDismissalActorIds += $teamDetail.node_id } else { $newErrorRecordParms = @{ - ErrorMessage = "Team $team does not have push or maintain permissions on repository $RepositoryName" + ErrorMessage = "Team '$team' does not have push or maintain permissions on repository '$RepositoryName'" ErrorId = 'DismissalTeamNoPermissions' ErrorCategory = [System.Management.Automation.ErrorCategory]::PermissionDenied TargetObject = $team } - $errorRecord = New-ErrorRecord @newErrorRecordParms + Write-Log -Exception $errorRecord -Level Error + Set-TelemetryException -Exception $ex -ErrorBucket $errorBucket -Properties $localTelemetryProperties + $PSCmdlet.ThrowTerminatingError($errorRecord) } - } } @@ -1508,42 +1509,21 @@ filter New-GitHubRepositoryBranchPatternProtectionRule { foreach ($team in $RestrictPushTeam) { - $hashbody = @{query = "query organization { organization(login: ""$OrganizationName"") " + - "{ team(slug: ""$team"") { id } } }" - } - - $description = "Querying $OrganizationName organization for team $team" - - Write-Debug -Message $description + $teamDetail = $orgTeams | Where-Object -Property Name -eq $RestrictPushTeam - $params = @{ - Body = ConvertTo-Json -InputObject $hashBody - Description = $description - AccessToken = $AccessToken - TelemetryEventName = 'GetGitHubTeamQ1' - TelemetryProperties = $telemetryProperties - } - - try - { - $result = Invoke-GHGraphQl @params - } - catch - { - $PSCmdlet.ThrowTerminatingError($_) - } - - if ([System.String]::IsNullOrEmpty($result.data.organization.team)) + if ($teamDetail.Count -eq 0) { $newErrorRecordParms = @{ - ErrorMessage = "Team $team not found in organization $OrganizationName" - ErrorId = 'DismissalTeamNotFound' + ErrorMessage = "Team '$team' not found in organization '$OrganizationName'" + ErrorId = 'RestrictPushTeamNotFound' ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound TargetObject = $team } - $errorRecord = New-ErrorRecord @newErrorRecordParms + Write-Log -Exception $errorRecord -Level Error + Set-TelemetryException -Exception $ex -ErrorBucket $errorBucket -Properties $localTelemetryProperties + $PSCmdlet.ThrowTerminatingError($errorRecord) } @@ -1553,23 +1533,33 @@ filter New-GitHubRepositoryBranchPatternProtectionRule RepositoryName = $repositoryName } - $teamPermission = Get-GitHubRepositoryTeamPermission @getGitHubRepositoryTeamPermissionParms + Write-Debug -Message "Getting GitHub Permissions for Team '$team' on Repository '$RepositoryName'" + try + { + $teamPermission = Get-GitHubRepositoryTeamPermission @getGitHubRepositoryTeamPermissionParms + } + catch + { + Write-Debug -Message "Team '$team' has no permissions on Repository '$RepositoryName'" + } if ($teamPermission.permissions.push -eq $true -or $teamPermission.permissions.maintain -eq $true) { - $restrictPushActorIds += $result.data.organization.team.id + $restrictPushActorIds += $teamDetail.node_id } else { $newErrorRecordParms = @{ - ErrorMessage = "Team $team does not have push or maintain permissions on repository $RepositoryName" - ErrorId = 'PushTeamNoPermissions' + ErrorMessage = "Team '$team' does not have push or maintain permissions on repository '$RepositoryName'" + ErrorId = 'RestrictPushTeamNoPermissions' ErrorCategory = [System.Management.Automation.ErrorCategory]::PermissionDenied TargetObject = $team } - $errorRecord = New-ErrorRecord @newErrorRecordParms + Write-Log -Exception $errorRecord -Level Error + Set-TelemetryException -Exception $ex -ErrorBucket $errorBucket -Properties $localTelemetryProperties + $PSCmdlet.ThrowTerminatingError($errorRecord) } } From 2fd31e0720ab25f44509ffba03d369711dca2d58 Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Sat, 27 Mar 2021 13:14:31 +0000 Subject: [PATCH 19/33] Refine debug/exception handling --- GitHubBranches.ps1 | 69 ++++++++++------------------------ GitHubGraphQl.ps1 | 28 +++++++------- Tests/GitHubBranches.tests.ps1 | 8 ++-- 3 files changed, 39 insertions(+), 66 deletions(-) diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 index 8f64cb22..ad88c49b 100644 --- a/GitHubBranches.ps1 +++ b/GitHubBranches.ps1 @@ -1269,11 +1269,9 @@ filter New-GitHubRepositoryBranchPatternProtectionRule $hashbody = @{query = "query repo { repository(name: ""$RepositoryName"", owner: ""$OwnerName"") { id } }" } - Write-Debug -Message "Querying Repository $RepositoryName, Owner $OwnerName" - $params = @{ Body = ConvertTo-Json -InputObject $hashBody - Description = "Querying $RepositoryName" + Description = "Querying Repository $RepositoryName, Owner $OwnerName" AccessToken = $AccessToken TelemetryEventName = 'Get-GitHubRepositoryQ1' TelemetryProperties = $telemetryProperties @@ -1344,13 +1342,9 @@ filter New-GitHubRepositoryBranchPatternProtectionRule { $hashbody = @{query = "query user { user(login: ""$user"") { id } }"} - $description = "Querying user $user" - - Write-Debug -Message $description - $params = @{ Body = ConvertTo-Json -InputObject $hashBody - Description = $description + Description = "Querying for user $user" AccessToken = $AccessToken TelemetryEventName = 'Get-GitHubUserQ1' TelemetryProperties = $telemetryProperties @@ -1373,7 +1367,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule { foreach ($team in $DismissalTeam) { - $teamDetail = $orgTeams | Where-Object -Property Name -eq $RestrictPushTeam + $teamDetail = $orgTeams | Where-Object -Property Name -eq $team if ($teamDetail.Count -eq 0) { @@ -1385,8 +1379,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule } $errorRecord = New-ErrorRecord @newErrorRecordParms - Write-Log -Exception $errorRecor -Level Error - Set-TelemetryException -Exception $ex -ErrorBucket $errorBucket -Properties $localTelemetryProperties + Write-Log -Exception $errorRecord -Level Error $PSCmdlet.ThrowTerminatingError($errorRecord) } @@ -1423,7 +1416,6 @@ filter New-GitHubRepositoryBranchPatternProtectionRule $errorRecord = New-ErrorRecord @newErrorRecordParms Write-Log -Exception $errorRecord -Level Error - Set-TelemetryException -Exception $ex -ErrorBucket $errorBucket -Properties $localTelemetryProperties $PSCmdlet.ThrowTerminatingError($errorRecord) } @@ -1480,13 +1472,9 @@ filter New-GitHubRepositoryBranchPatternProtectionRule { $hashbody = @{query = "query user { user(login: ""$user"") { id } }" } - $description = "Querying User $user" - - Write-Debug -Message $description - $params = @{ Body = ConvertTo-Json -InputObject $hashBody - Description = $description + Description = "Querying for User $user" AccessToken = $AccessToken TelemetryEventName = 'GetGitHubUserQ1' TelemetryProperties = $telemetryProperties @@ -1509,7 +1497,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule { foreach ($team in $RestrictPushTeam) { - $teamDetail = $orgTeams | Where-Object -Property Name -eq $RestrictPushTeam + $teamDetail = $orgTeams | Where-Object -Property Name -eq $team if ($teamDetail.Count -eq 0) { @@ -1522,7 +1510,6 @@ filter New-GitHubRepositoryBranchPatternProtectionRule $errorRecord = New-ErrorRecord @newErrorRecordParms Write-Log -Exception $errorRecord -Level Error - Set-TelemetryException -Exception $ex -ErrorBucket $errorBucket -Properties $localTelemetryProperties $PSCmdlet.ThrowTerminatingError($errorRecord) } @@ -1558,7 +1545,6 @@ filter New-GitHubRepositoryBranchPatternProtectionRule $errorRecord = New-ErrorRecord @newErrorRecordParms Write-Log -Exception $errorRecord -Level Error - Set-TelemetryException -Exception $ex -ErrorBucket $errorBucket -Properties $localTelemetryProperties $PSCmdlet.ThrowTerminatingError($errorRecord) } @@ -1571,13 +1557,9 @@ filter New-GitHubRepositoryBranchPatternProtectionRule { $hashbody = @{query = "query app { marketplaceListing(slug: ""$app"") { app { id } } }" } - $description = "Querying for app $app" - - Write-Debug -Message $description - $params = @{ Body = ConvertTo-Json -InputObject $hashBody - Description = $description + Description = "Querying for app $app" AccessToken = $AccessToken TelemetryEventName = 'Get-GitHubAppQ1' TelemetryProperties = $telemetryProperties @@ -1599,13 +1581,15 @@ filter New-GitHubRepositoryBranchPatternProtectionRule else { $newErrorRecordParms = @{ - ErrorMessage = "App $app not found in marketplace" + ErrorMessage = "App '$app' not found in GitHub Marketplace" ErrorId = 'RestictPushAppNotFound' ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound TargetObject = $app } $errorRecord = New-ErrorRecord @newErrorRecordParms + Write-Log -Exception $errorRecord -Level Error + $PSCmdlet.ThrowTerminatingError($errorRecord) } } @@ -1630,12 +1614,8 @@ filter New-GitHubRepositoryBranchPatternProtectionRule "{ clientMutationId } } " } - $description = "Setting $BranchPatternName branch protection status for $RepositoryName" $body = ConvertTo-Json -InputObject $hashBody - Write-Debug -Message $description - Write-Debug -Message "Query: $body" - if (-not $PSCmdlet.ShouldProcess( "Owner '$OwnerName', Repository '$RepositoryName'", "Create '$BranchPatternName' branch pattern GitHub Repository Branch Protection Rule")) @@ -1645,7 +1625,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule $params = @{ Body = $body - Description = $description + Description = "Creating $BranchPatternName branch protection rule for $RepositoryName" AccessToken = $AccessToken TelemetryEventName = $MyInvocation.MyCommand.Name TelemetryProperties = $telemetryProperties @@ -1772,14 +1752,9 @@ filter Get-GitHubRepositoryBranchPatternProtectionRule "owner: ""$OwnerName"") { branchProtectionRules(first: $script:MaxProtectionRules) { nodes { " + "$branchProtectionRuleFields } } } }"} - $description = "Querying $RepositoryName repository for branch protection rules" - - Write-Debug -Message $description - Write-Debug -Message "Query: $($hashbody.query)" - $params = @{ Body = ConvertTo-Json -InputObject $hashBody - Description = $description + Description = "Querying $RepositoryName repository for branch protection rules" AccessToken = $AccessToken TelemetryEventName = $MyInvocation.MyCommand.Name TelemetryProperties = $telemetryProperties @@ -1810,13 +1785,15 @@ filter Get-GitHubRepositoryBranchPatternProtectionRule if (!$rule -and $PSBoundParameters.ContainsKey('BranchPatternName')) { $newErrorRecordParms = @{ - ErrorMessage = "Branch Protection Rule '$BranchPatternName' not found on repository $RepositoryName" + ErrorMessage = "Branch Protection Rule '$BranchPatternName' not found on repository '$RepositoryName'" ErrorId = 'BranchProtectionRuleNotFound' ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound TargetObject = $BranchPatternName } $errorRecord = New-ErrorRecord @newErrorRecordParms + Write-Log -Exception $errorRecord -Level Error + $PSCmdlet.ThrowTerminatingError($errorRecord) } @@ -1924,13 +1901,9 @@ filter Remove-GitHubRepositoryBranchPatternProtectionRule "owner: ""$OwnerName"") { branchProtectionRules(first: $script:MaxProtectionRules) { nodes { id pattern } } } }" } - $description = "Querying $RepositoryName repository for branch protection rules" - - Write-Debug -Message $description - $params = @{ Body = ConvertTo-Json -InputObject $hashBody - Description = $description + Description = "Querying $RepositoryName repository for branch protection rules" AccessToken = $AccessToken TelemetryEventName = 'Get-GitHubRepositoryQ1' TelemetryProperties = $telemetryProperties @@ -1954,13 +1927,15 @@ filter Remove-GitHubRepositoryBranchPatternProtectionRule if (!$ruleId) { $newErrorRecordParms = @{ - ErrorMessage = "Branch Protection Rule '$BranchPatternName' not found on repository $RepositoryName" + ErrorMessage = "Branch Protection Rule '$BranchPatternName' not found on repository '$RepositoryName'" ErrorId = 'BranchProtectionRuleNotFound' ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound TargetObject = $BranchPatternName } $errorRecord = New-ErrorRecord @newErrorRecordParms + Write-Log -Exception $errorRecord -Level Error + $PSCmdlet.ThrowTerminatingError($errorRecord) } @@ -1973,12 +1948,8 @@ filter Remove-GitHubRepositoryBranchPatternProtectionRule "{ branchProtectionRuleId: ""$ruleId"" } ) { clientMutationId } }" } - $description = "Removing $BranchPatternName branch protection rule for $RepositoryName" $body = ConvertTo-Json -InputObject $hashBody - Write-Debug -Message $description - Write-Debug -Message "Query: $body" - if (-not $PSCmdlet.ShouldProcess("'$BranchPatternName' branch of repository '$RepositoryName'", 'Remove GitHub Repository Branch Protection Rule')) { @@ -1987,7 +1958,7 @@ filter Remove-GitHubRepositoryBranchPatternProtectionRule $params = @{ Body = $body - Description = $description + Description = "Removing $BranchPatternName branch protection rule for $RepositoryName" AccessToken = $AccessToken TelemetryEventName = $MyInvocation.MyCommand.Name TelemetryProperties = $telemetryProperties diff --git a/GitHubGraphQl.ps1 b/GitHubGraphQl.ps1 index 29bd9f8c..c45c1858 100644 --- a/GitHubGraphQl.ps1 +++ b/GitHubGraphQl.ps1 @@ -106,6 +106,14 @@ function Invoke-GHGraphQl Write-Log -Message $Description -Level Verbose Write-Log -Message "Accessing [$method] $url [Timeout = $timeOut]" -Level Verbose + if (Get-GitHubConfiguration -Name LogRequestBody) + { + Write-Log -Message $Body -Level Verbose + } + + Write-Debug -Message $Description + Write-Debug -Message "Query: $Body" + $bodyAsBytes = [System.Text.Encoding]::UTF8.GetBytes($Body) $params = @{ @@ -118,11 +126,6 @@ function Invoke-GHGraphQl TimeoutSec = $timeOut } - if (Get-GitHubConfiguration -Name LogRequestBody) - { - Write-Log -Message $Body -Level Verbose - } - # Disable Progress Bar in function scope during Invoke-WebRequest $ProgressPreference = 'SilentlyContinue' @@ -225,11 +228,7 @@ function Invoke-GHGraphQl $requestId = '' - if ($ex.Response.PSTypeNames[0] -eq 'System.Net.Http.HttpResponseMessage') - { - $requestId = ($ex.Response.Headers | Where-Object -Property Key -eq 'X-GitHub-Request-Id').Value - } - elseif ($ex.Response.PSTypeNames[0] -eq 'System.Net.HttpWebResponse') + if ($ex.Response.PSTypeNames[0] -eq 'System.Net.HttpWebResponse') { if (($ex.Response.Headers.Count -gt 0) -and (-not [System.String]::IsNullOrEmpty($ex.Response.Headers['X-GitHub-Request-Id']))) @@ -237,6 +236,10 @@ function Invoke-GHGraphQl $requestId = $ex.Response.Headers['X-GitHub-Request-Id'] } } + elseif ($ex.Response.PSTypeNames[0] -eq 'System.Net.Http.HttpResponseMessage') + { + $requestId = ($ex.Response.Headers | Where-Object -Property Key -eq 'X-GitHub-Request-Id').Value + } if (-not [System.String]::IsNullOrEmpty($requestId)) { @@ -283,8 +286,6 @@ function Invoke-GHGraphQl } finally { - Write-Debug -Message "Processing Invoke-WebRequest 'finally' block" - # Restore original security protocol [Net.ServicePointManager]::SecurityProtocol = $originalSecurityProtocol } @@ -297,6 +298,8 @@ function Invoke-GHGraphQl Set-TelemetryEvent -EventName $TelemetryEventName -Properties $localTelemetryProperties -Metrics $telemetryMetrics } + Write-Debug -Message "GraphQl result: '$($result.Content)'" + $graphQlResult = $result.Content | ConvertFrom-Json if ($graphQlResult.errors) @@ -361,7 +364,6 @@ function Invoke-GHGraphQl } else { - Write-Debug -Message "Returning GraphQl result '$graphQLResult'" return $graphQlResult } } diff --git a/Tests/GitHubBranches.tests.ps1 b/Tests/GitHubBranches.tests.ps1 index 2ed2b4a1..8017f84f 100644 --- a/Tests/GitHubBranches.tests.ps1 +++ b/Tests/GitHubBranches.tests.ps1 @@ -1126,7 +1126,7 @@ try } It 'Should throw the correct exception' { - $errorMessage = "Team $mockTeamName not found in organization $OrganizationName" + $errorMessage = "Team '$mockTeamName' not found in organization '$OrganizationName'" { New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms } | Should -Throw $errorMessage } @@ -1144,7 +1144,7 @@ try } It 'Should throw the correct exception' { - $errorMessage = "Team $pullTeamName does not have push or maintain permissions on repository $repoName" + $errorMessage = "Team '$pullTeamName' does not have push or maintain permissions on repository '$repoName'" { New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms } | Should -Throw $errorMessage } @@ -1200,7 +1200,7 @@ try } It 'Should throw the correct exception' { - $errorMessage = "Team $mockTeamName not found in organization $OrganizationName" + $errorMessage = "Team '$mockTeamName' not found in organization '$OrganizationName'" { New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms } | Should -Throw $errorMessage } @@ -1218,7 +1218,7 @@ try } It 'Should throw the correct exception' { - $errorMessage = "Team $pullTeamName does not have push or maintain permissions on repository $repoName" + $errorMessage = "Team '$pullTeamName' does not have push or maintain permissions on repository '$repoName'" { New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms } | Should -Throw $errorMessage } From fa9979e71a63e0f34482c863fbf65d814efb9d08 Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Sat, 27 Mar 2021 13:43:15 +0000 Subject: [PATCH 20/33] Use TeamSlug for Get-GitHubRepositoryTeamPerm --- GitHubBranches.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 index ad88c49b..832de488 100644 --- a/GitHubBranches.ps1 +++ b/GitHubBranches.ps1 @@ -1385,7 +1385,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule } $getGitHubRepositoryTeamPermissionParms = @{ - TeamName = $team + TeamSlug = $teamDetail.TeamSlug OwnerName = $ownerName RepositoryName = $repositoryName } @@ -1515,7 +1515,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule } $getGitHubRepositoryTeamPermissionParms = @{ - TeamName = $team + TeamSlug = $teamDetail.TeamSlug OwnerName = $ownerName RepositoryName = $repositoryName } From c57fee3c913d36ca93e2c60ed4cbd404d0a62098 Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Sat, 27 Mar 2021 13:44:43 +0000 Subject: [PATCH 21/33] Suppress Verbose in nested function calls --- GitHubBranches.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 index 832de488..06d56189 100644 --- a/GitHubBranches.ps1 +++ b/GitHubBranches.ps1 @@ -1299,7 +1299,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule try { - $orgTeams = Get-GitHubTeam -OrganizationName $OrganizationName + $orgTeams = Get-GitHubTeam -OrganizationName $OrganizationName -Verbose:$false } catch { @@ -1388,6 +1388,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule TeamSlug = $teamDetail.TeamSlug OwnerName = $ownerName RepositoryName = $repositoryName + Verbose = $false } Write-Debug -Message "Getting GitHub Permissions for Team '$team' on Repository '$RepositoryName'" @@ -1518,6 +1519,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule TeamSlug = $teamDetail.TeamSlug OwnerName = $ownerName RepositoryName = $repositoryName + Verbose = $false } Write-Debug -Message "Getting GitHub Permissions for Team '$team' on Repository '$RepositoryName'" From d9ac0eee62e01e048d425be1b5d99c31df792f89 Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Sat, 27 Mar 2021 14:01:33 +0000 Subject: [PATCH 22/33] Refine messsages --- GitHubBranches.ps1 | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 index 06d56189..dfd04c27 100644 --- a/GitHubBranches.ps1 +++ b/GitHubBranches.ps1 @@ -1391,7 +1391,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule Verbose = $false } - Write-Debug -Message "Getting GitHub Permissions for Team '$team' on Repository '$RepositoryName'" + Write-Debug -Message "Getting GitHub Permissions for Team '$team' on Repository '$OwnerName/$RepositoryName'" try { @@ -1399,7 +1399,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule } catch { - Write-Debug -Message "Team '$team' has no permissions on Repository '$RepositoryName'" + Write-Debug -Message "Team '$team' has no permissions on Repository '$OwnerName/$RepositoryName'" } if (($teamPermission.permissions.push -eq $true) -or ($teamPermission.permissions.maintain -eq $true)) @@ -1409,7 +1409,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule else { $newErrorRecordParms = @{ - ErrorMessage = "Team '$team' does not have push or maintain permissions on repository '$RepositoryName'" + ErrorMessage = "Team '$team' does not have push or maintain permissions on repository '$OwnerName/$RepositoryName'" ErrorId = 'DismissalTeamNoPermissions' ErrorCategory = [System.Management.Automation.ErrorCategory]::PermissionDenied TargetObject = $team @@ -1522,14 +1522,14 @@ filter New-GitHubRepositoryBranchPatternProtectionRule Verbose = $false } - Write-Debug -Message "Getting GitHub Permissions for Team '$team' on Repository '$RepositoryName'" + Write-Debug -Message "Getting GitHub Permissions for Team '$team' on Repository '$OwnerName/$RepositoryName'" try { $teamPermission = Get-GitHubRepositoryTeamPermission @getGitHubRepositoryTeamPermissionParms } catch { - Write-Debug -Message "Team '$team' has no permissions on Repository '$RepositoryName'" + Write-Debug -Message "Team '$team' has no permissions on Repository '$OwnerName/$RepositoryName'" } if ($teamPermission.permissions.push -eq $true -or $teamPermission.permissions.maintain -eq $true) @@ -1539,7 +1539,7 @@ filter New-GitHubRepositoryBranchPatternProtectionRule else { $newErrorRecordParms = @{ - ErrorMessage = "Team '$team' does not have push or maintain permissions on repository '$RepositoryName'" + ErrorMessage = "Team '$team' does not have push or maintain permissions on repository '$OwnerName/$RepositoryName'" ErrorId = 'RestrictPushTeamNoPermissions' ErrorCategory = [System.Management.Automation.ErrorCategory]::PermissionDenied TargetObject = $team @@ -1619,15 +1619,15 @@ filter New-GitHubRepositoryBranchPatternProtectionRule $body = ConvertTo-Json -InputObject $hashBody if (-not $PSCmdlet.ShouldProcess( - "Owner '$OwnerName', Repository '$RepositoryName'", - "Create '$BranchPatternName' branch pattern GitHub Repository Branch Protection Rule")) + "$OwnerName/$RepositoryName", + "Create GitHub Repository Branch Pattern Protection Rule '$BranchPatternName'")) { return } $params = @{ Body = $body - Description = "Creating $BranchPatternName branch protection rule for $RepositoryName" + Description = "Creating GitHub Repository Branch Pattern Protection Rule '$BranchPatternName' on $OwnerName/$RepositoryName" AccessToken = $AccessToken TelemetryEventName = $MyInvocation.MyCommand.Name TelemetryProperties = $telemetryProperties @@ -1756,7 +1756,7 @@ filter Get-GitHubRepositoryBranchPatternProtectionRule $params = @{ Body = ConvertTo-Json -InputObject $hashBody - Description = "Querying $RepositoryName repository for branch protection rules" + Description = "Querying $OwnerName/$RepositoryName repository for branch protection rules" AccessToken = $AccessToken TelemetryEventName = $MyInvocation.MyCommand.Name TelemetryProperties = $telemetryProperties @@ -1787,7 +1787,7 @@ filter Get-GitHubRepositoryBranchPatternProtectionRule if (!$rule -and $PSBoundParameters.ContainsKey('BranchPatternName')) { $newErrorRecordParms = @{ - ErrorMessage = "Branch Protection Rule '$BranchPatternName' not found on repository '$RepositoryName'" + ErrorMessage = "Branch Protection Rule '$BranchPatternName' not found on repository '$OwnerName/$RepositoryName'" ErrorId = 'BranchProtectionRuleNotFound' ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound TargetObject = $BranchPatternName @@ -1905,7 +1905,7 @@ filter Remove-GitHubRepositoryBranchPatternProtectionRule $params = @{ Body = ConvertTo-Json -InputObject $hashBody - Description = "Querying $RepositoryName repository for branch protection rules" + Description = "Querying $OwnerName/$RepositoryName repository for branch protection rules" AccessToken = $AccessToken TelemetryEventName = 'Get-GitHubRepositoryQ1' TelemetryProperties = $telemetryProperties @@ -1929,7 +1929,7 @@ filter Remove-GitHubRepositoryBranchPatternProtectionRule if (!$ruleId) { $newErrorRecordParms = @{ - ErrorMessage = "Branch Protection Rule '$BranchPatternName' not found on repository '$RepositoryName'" + ErrorMessage = "Branch Protection Rule '$BranchPatternName' not found on repository '$OwnerName/$RepositoryName'" ErrorId = 'BranchProtectionRuleNotFound' ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound TargetObject = $BranchPatternName @@ -1952,15 +1952,15 @@ filter Remove-GitHubRepositoryBranchPatternProtectionRule $body = ConvertTo-Json -InputObject $hashBody - if (-not $PSCmdlet.ShouldProcess("'$BranchPatternName' branch of repository '$RepositoryName'", - 'Remove GitHub Repository Branch Protection Rule')) + if (-not $PSCmdlet.ShouldProcess("$OwnerName/$RepositoryName", + "Remove GitHub Repository Branch Pattern Protection Rule '$BranchPatternName'")) { return } $params = @{ Body = $body - Description = "Removing $BranchPatternName branch protection rule for $RepositoryName" + Description = "Removing GitHub Repository Branch Pattern Protection Rule '$BranchPatternName' from $OwnerName/$RepositoryName" AccessToken = $AccessToken TelemetryEventName = $MyInvocation.MyCommand.Name TelemetryProperties = $telemetryProperties From 1f02919ca75052d614a80f0cc524b62218a89e56 Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Sat, 27 Mar 2021 14:02:26 +0000 Subject: [PATCH 23/33] Reorder InvokeWebRequest cmds --- GitHubGraphQl.ps1 | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/GitHubGraphQl.ps1 b/GitHubGraphQl.ps1 index c45c1858..416af8cb 100644 --- a/GitHubGraphQl.ps1 +++ b/GitHubGraphQl.ps1 @@ -116,16 +116,6 @@ function Invoke-GHGraphQl $bodyAsBytes = [System.Text.Encoding]::UTF8.GetBytes($Body) - $params = @{ - Uri = $url - Method = $method - Headers = $headers - Body = $bodyAsBytes - UseDefaultCredentials = $true - UseBasicParsing = $true - TimeoutSec = $timeOut - } - # Disable Progress Bar in function scope during Invoke-WebRequest $ProgressPreference = 'SilentlyContinue' @@ -135,9 +125,20 @@ function Invoke-GHGraphQl # Enforce TLS v1.2 Security Protocol [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + $invokeWebRequestParms = @{ + Uri = $url + Method = $method + Headers = $headers + Body = $bodyAsBytes + UseDefaultCredentials = $true + UseBasicParsing = $true + TimeoutSec = $timeOut + Verbose = $false + } + try { - $result = Invoke-WebRequest @params + $result = Invoke-WebRequest @invokeWebRequestParms } catch { From 432c10408d1e8254e6dbc42c1486152fdd7d96d6 Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Sat, 27 Mar 2021 14:04:16 +0000 Subject: [PATCH 24/33] Change GraphQL logging to debug level --- GitHubGraphQl.ps1 | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/GitHubGraphQl.ps1 b/GitHubGraphQl.ps1 index 416af8cb..d8501fb2 100644 --- a/GitHubGraphQl.ps1 +++ b/GitHubGraphQl.ps1 @@ -103,17 +103,14 @@ function Invoke-GHGraphQl $timeOut = Get-GitHubConfiguration -Name WebRequestTimeoutSec $method = 'Post' - Write-Log -Message $Description -Level Verbose - Write-Log -Message "Accessing [$method] $url [Timeout = $timeOut]" -Level Verbose + Write-Log -Message $Description -Level Debug + Write-Log -Message "Accessing [$method] $url [Timeout = $timeOut]" -Level Debug if (Get-GitHubConfiguration -Name LogRequestBody) { - Write-Log -Message $Body -Level Verbose + Write-Log -Message $Body -Level Debug } - Write-Debug -Message $Description - Write-Debug -Message "Query: $Body" - $bodyAsBytes = [System.Text.Encoding]::UTF8.GetBytes($Body) # Disable Progress Bar in function scope during Invoke-WebRequest @@ -250,7 +247,7 @@ function Invoke-GHGraphQl $requestIdMessage += "RequestId: $requestId" $errorMessage += $requestIdMessage - Write-Log -Message $requestIdMessage -Level Verbose + Write-Log -Message $requestIdMessage -Level Debug } } @@ -296,7 +293,7 @@ function Invoke-GHGraphQl if (-not [String]::IsNullOrEmpty($TelemetryEventName)) { $telemetryMetrics = @{ 'Duration' = $stopwatch.Elapsed.TotalSeconds } - Set-TelemetryEvent -EventName $TelemetryEventName -Properties $localTelemetryProperties -Metrics $telemetryMetrics + Set-TelemetryEvent -EventName $TelemetryEventName -Properties $localTelemetryProperties -Metrics $telemetryMetrics -Verbose:$false } Write-Debug -Message "GraphQl result: '$($result.Content)'" @@ -343,12 +340,10 @@ function Invoke-GHGraphQl { $requestId = $result.Headers['X-GitHub-Request-Id'] - Write-Debug -Message "GitHub RequestID '$requestId' in response header" - $requestIdMessage += "RequestId: $requestId" $errorMessage += $requestIdMessage - Write-Log -Message $requestIdMessage -Level Verbose + Write-Log -Message $requestIdMessage -Level Debug } $newErrorRecordParms = @{ From c9f6e5144c8ebdb1af5e857014c005a83a099ed4 Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Tue, 13 Jul 2021 08:28:16 +0100 Subject: [PATCH 25/33] Fix team permissions test exception --- Tests/GitHubBranches.tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/GitHubBranches.tests.ps1 b/Tests/GitHubBranches.tests.ps1 index 8017f84f..21d55f6d 100644 --- a/Tests/GitHubBranches.tests.ps1 +++ b/Tests/GitHubBranches.tests.ps1 @@ -1144,7 +1144,7 @@ try } It 'Should throw the correct exception' { - $errorMessage = "Team '$pullTeamName' does not have push or maintain permissions on repository '$repoName'" + $errorMessage = "Team '$pullTeamName' does not have push or maintain permissions on repository '$OrganizationName/$repoName'" { New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms } | Should -Throw $errorMessage } @@ -1218,7 +1218,7 @@ try } It 'Should throw the correct exception' { - $errorMessage = "Team '$pullTeamName' does not have push or maintain permissions on repository '$repoName'" + $errorMessage = "Team '$pullTeamName' does not have push or maintain permissions on repository '$OrganizationName/$repoName'" { New-GitHubRepositoryBranchPatternProtectionRule @newGitHubRepositoryBranchPatternProtectionParms } | Should -Throw $errorMessage } From cef1077b779ae87db09cc72606c183d01dc23c55 Mon Sep 17 00:00:00 2001 From: Simon Heather Date: Tue, 13 Jul 2021 08:32:45 +0100 Subject: [PATCH 26/33] Fix invoke-ghgraphql exception message --- Tests/GitHubGraphQl.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/GitHubGraphQl.Tests.ps1 b/Tests/GitHubGraphQl.Tests.ps1 index 67b25549..ac4f1dac 100644 --- a/Tests/GitHubGraphQl.Tests.ps1 +++ b/Tests/GitHubGraphQl.Tests.ps1 @@ -131,7 +131,7 @@ try $exceptionMessage1 = '*The remote server returned an error: (401) Unauthorized*' } - $exceptionMessage2 = '*This endpoint requires you to be authenticated. | https://docs.github.com/v3/#authentication*' + $exceptionMessage2 = '*This endpoint requires you to be authenticated.*' Mock -CommandName Get-AccessToken -ModuleName $script:moduleName } From 35a4202a048a20c7cf14fe5ef9a0ec27eb92aade Mon Sep 17 00:00:00 2001 From: Howard Wolosky Date: Wed, 14 Jul 2021 07:57:48 -0700 Subject: [PATCH 27/33] Update Tests/GitHubGraphQl.Tests.ps1 Fixing test for Mac/Linux on PSCore --- Tests/GitHubGraphQl.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/GitHubGraphQl.Tests.ps1 b/Tests/GitHubGraphQl.Tests.ps1 index ac4f1dac..2f2a2551 100644 --- a/Tests/GitHubGraphQl.Tests.ps1 +++ b/Tests/GitHubGraphQl.Tests.ps1 @@ -56,7 +56,7 @@ try if ($PSVersionTable.PSEdition -eq 'Core') { - $exceptionMessage = 'No such host is known' + $exceptionMessage = "*$($testHostName)*" $categoryInfo = 'InvalidOperation' $targetName = "*$testHostName*" } From a0383f5287f95c0d5c1531789c45e589bbaa5c67 Mon Sep 17 00:00:00 2001 From: Howard Wolosky Date: Wed, 14 Jul 2021 11:55:43 -0700 Subject: [PATCH 28/33] Update Tests/GitHubGraphQl.Tests.ps1 --- Tests/GitHubGraphQl.Tests.ps1 | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Tests/GitHubGraphQl.Tests.ps1 b/Tests/GitHubGraphQl.Tests.ps1 index 2f2a2551..be3b52af 100644 --- a/Tests/GitHubGraphQl.Tests.ps1 +++ b/Tests/GitHubGraphQl.Tests.ps1 @@ -56,7 +56,14 @@ try if ($PSVersionTable.PSEdition -eq 'Core') { - $exceptionMessage = "*$($testHostName)*" + if ($IsWindows) + { + $exceptionMessage = "No such host is known. ($($testHostName):443)" + } + else + { + $exceptionMessage = "'nodename nor servname provided, or not known ($($testHostName):443)'" + } $categoryInfo = 'InvalidOperation' $targetName = "*$testHostName*" } From 65db6fbce471a33fea80a216e834ac964d5d175b Mon Sep 17 00:00:00 2001 From: Howard Wolosky Date: Wed, 14 Jul 2021 15:34:44 -0700 Subject: [PATCH 29/33] Update Tests/GitHubGraphQl.Tests.ps1 --- Tests/GitHubGraphQl.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/GitHubGraphQl.Tests.ps1 b/Tests/GitHubGraphQl.Tests.ps1 index be3b52af..6d2c2f9b 100644 --- a/Tests/GitHubGraphQl.Tests.ps1 +++ b/Tests/GitHubGraphQl.Tests.ps1 @@ -62,7 +62,7 @@ try } else { - $exceptionMessage = "'nodename nor servname provided, or not known ($($testHostName):443)'" + $exceptionMessage = "nodename nor servname provided, or not known ($($testHostName):443)" } $categoryInfo = 'InvalidOperation' $targetName = "*$testHostName*" From 4edc805a6b59157ece8fa14aefcd31d02c01665e Mon Sep 17 00:00:00 2001 From: Howard Wolosky Date: Wed, 14 Jul 2021 17:25:44 -0700 Subject: [PATCH 30/33] Update Tests/GitHubGraphQl.Tests.ps1 --- Tests/GitHubGraphQl.Tests.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Tests/GitHubGraphQl.Tests.ps1 b/Tests/GitHubGraphQl.Tests.ps1 index 6d2c2f9b..52a6cb92 100644 --- a/Tests/GitHubGraphQl.Tests.ps1 +++ b/Tests/GitHubGraphQl.Tests.ps1 @@ -60,10 +60,14 @@ try { $exceptionMessage = "No such host is known. ($($testHostName):443)" } - else + elseif ($IsMacOS) { $exceptionMessage = "nodename nor servname provided, or not known ($($testHostName):443)" } + else + { + $exceptionMessage = "Resource temporarily unavailable ($($testHostName):443)" + } $categoryInfo = 'InvalidOperation' $targetName = "*$testHostName*" } From 2cf39e653a5ac45869b4f0545a9fa99a8fbeefcf Mon Sep 17 00:00:00 2001 From: Howard Wolosky Date: Wed, 14 Jul 2021 19:21:21 -0700 Subject: [PATCH 31/33] Update Tests/GitHubGraphQl.Tests.ps1 --- Tests/GitHubGraphQl.Tests.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/GitHubGraphQl.Tests.ps1 b/Tests/GitHubGraphQl.Tests.ps1 index 52a6cb92..3d7755c6 100644 --- a/Tests/GitHubGraphQl.Tests.ps1 +++ b/Tests/GitHubGraphQl.Tests.ps1 @@ -90,6 +90,7 @@ try { Invoke-GHGraphQl @invokeGHGraphQlParms } | Should -Throw $exceptionMessage + $Error[0].Exception.Message | Should -BeLike $exceptionMessage $Error[0].CategoryInfo.Category | Should -Be $categoryInfo $Error[0].CategoryInfo.TargetName | Should -BeLike $targetName $Error[0].FullyQualifiedErrorId | Should -BeLike '*Invoke-GHGraphQl' From b4bda8d27b5a4734c47751f3352d7f92673676ff Mon Sep 17 00:00:00 2001 From: Howard Wolosky Date: Wed, 14 Jul 2021 19:21:29 -0700 Subject: [PATCH 32/33] Update Tests/GitHubGraphQl.Tests.ps1 --- Tests/GitHubGraphQl.Tests.ps1 | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/Tests/GitHubGraphQl.Tests.ps1 b/Tests/GitHubGraphQl.Tests.ps1 index 3d7755c6..75e4aba6 100644 --- a/Tests/GitHubGraphQl.Tests.ps1 +++ b/Tests/GitHubGraphQl.Tests.ps1 @@ -56,18 +56,13 @@ try if ($PSVersionTable.PSEdition -eq 'Core') { - if ($IsWindows) - { - $exceptionMessage = "No such host is known. ($($testHostName):443)" - } - elseif ($IsMacOS) - { - $exceptionMessage = "nodename nor servname provided, or not known ($($testHostName):443)" - } - else - { - $exceptionMessage = "Resource temporarily unavailable ($($testHostName):443)" - } + # The exception message varies per platform. We could special-case it, but the exact message + # may change over time and the module itself doesn't care about the specific message. + # We'll just do a best-case match. + # Windows: "No such host is known. ($($testHostName):443)" + # Mac: "nodename nor servname provided, or not known ($($testHostName):443)" + # Linux: "Resource temporarily unavailable ($($testHostName):443)" + $exceptionMessage = "*$testHostName*" $categoryInfo = 'InvalidOperation' $targetName = "*$testHostName*" } From d6f8fd023c50a78035ee81dda7f4607a821245f3 Mon Sep 17 00:00:00 2001 From: Howard Wolosky Date: Wed, 14 Jul 2021 19:21:38 -0700 Subject: [PATCH 33/33] Update Tests/GitHubGraphQl.Tests.ps1 --- Tests/GitHubGraphQl.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/GitHubGraphQl.Tests.ps1 b/Tests/GitHubGraphQl.Tests.ps1 index 75e4aba6..0c94719b 100644 --- a/Tests/GitHubGraphQl.Tests.ps1 +++ b/Tests/GitHubGraphQl.Tests.ps1 @@ -83,7 +83,7 @@ try Body = $testBody } { Invoke-GHGraphQl @invokeGHGraphQlParms } | - Should -Throw $exceptionMessage + Should -Throw $Error[0].Exception.Message | Should -BeLike $exceptionMessage $Error[0].CategoryInfo.Category | Should -Be $categoryInfo