From 9e7a3315e9093721f2bb8ea685f01d3028e4343c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Sep 2025 19:14:28 +0200 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=AA=B2[Fix]:=20Piping=20`GitHubSecret?= =?UTF-8?q?`=20and=20`GitHubVariable`=20objects=20to=20`Remove-GitHubSecre?= =?UTF-8?q?t`=20and=20`Remove-GitHubVariable`=20(#499)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request improves the handling and testing of secret and variable removal in the GitHub module. The main focus is on making the removal functions (`Remove-GitHubSecret`, `Remove-GitHubVariable`) more robust and scope-aware, and on expanding the test coverage to ensure correct behavior across different usage patterns and scopes. - Fixes #388 Key changes include: ### Functional improvements to removal logic * Updated `Remove-GitHubSecret.ps1` and `Remove-GitHubVariable.ps1` to dispatch removal actions based on the `Scope` property of each item, ensuring the correct removal function is called for environment, repository, or organization scopes, and providing clear error handling for unsupported scopes. ### Documentation and class property clarifications * Improved property descriptions in `GitHubSecret.ps1` and `GitHubVariable.ps1` to clarify that properties refer to where the secret or variable is stored, enhancing code readability and maintainability. ### Expanded and improved test coverage * Refactored and expanded tests in `Secrets.Tests.ps1` and `Variables.Tests.ps1` to: - Add separate tests for pipeline-based removal using both direct pipeline and variable assignment approaches. - Ensure secrets and variables are created and removed correctly in all supported scopes. - Add logging and verification steps to improve test clarity and debugging. These changes make the module's behavior more predictable and easier to test, especially when handling secrets and variables in different scopes and using pipeline operations. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: MariusStorhaug <17722253+MariusStorhaug@users.noreply.github.com> Co-authored-by: Marius Storhaug --- src/classes/public/Secrets/GitHubSecret.ps1 | 18 ++-- .../public/Variables/GitHubVariable.ps1 | 6 +- .../public/Secrets/Remove-GitHubSecret.ps1 | 39 ++++++-- .../Variables/Remove-GitHubVariable.ps1 | 39 ++++++-- tests/Secrets.Tests.ps1 | 85 ++++++++++++++-- tests/Variables.Tests.ps1 | 98 ++++++++++++++++--- 6 files changed, 237 insertions(+), 48 deletions(-) diff --git a/src/classes/public/Secrets/GitHubSecret.ps1 b/src/classes/public/Secrets/GitHubSecret.ps1 index 61b8c9160..27c73ead9 100644 --- a/src/classes/public/Secrets/GitHubSecret.ps1 +++ b/src/classes/public/Secrets/GitHubSecret.ps1 @@ -1,29 +1,29 @@ class GitHubSecret { - # The key ID of the public key. + # The name of the secret. [string] $Name - # The scope of the variable, organization, repository, or environment. + # The scope of the secret, organization, repository, or environment. [string] $Scope - # The name of the organization or user the Public Key is associated with. + # The name of the organization or user the secret is stored in. [string] $Owner - # The name of the repository the Public Key is associated with. + # The name of the repository the secret is stored in. [string] $Repository - # The name of the environment the Public Key is associated with. + # The name of the environment the secret is stored in. [string] $Environment - # The date and time the variable was created. + # The date and time the secret was created. [datetime] $CreatedAt - # The date and time the variable was last updated. + # The date and time the secret was last updated. [datetime] $UpdatedAt - # The visibility of the variable. + # The visibility of the secret. [string] $Visibility - # The ids of the repositories that the variable is visible to. + # The ids of the repositories that the secret is visible to. [GitHubRepository[]] $SelectedRepositories GitHubSecret() {} diff --git a/src/classes/public/Variables/GitHubVariable.ps1 b/src/classes/public/Variables/GitHubVariable.ps1 index 6af9e2c0a..412d31c15 100644 --- a/src/classes/public/Variables/GitHubVariable.ps1 +++ b/src/classes/public/Variables/GitHubVariable.ps1 @@ -8,13 +8,13 @@ # The scope of the variable, organization, repository, or environment. [string] $Scope - # The name of the organization or user the variable is associated with. + # The name of the organization or user the variable is stored in. [string] $Owner - # The name of the repository the variable is associated with. + # The name of the repository the variable is stored in. [string] $Repository - # The name of the environment the variable is associated with. + # The name of the environment the variable is stored in. [string] $Environment # The date and time the variable was created. diff --git a/src/functions/public/Secrets/Remove-GitHubSecret.ps1 b/src/functions/public/Secrets/Remove-GitHubSecret.ps1 index 2d4f1a4b4..f5ea7b3eb 100644 --- a/src/functions/public/Secrets/Remove-GitHubSecret.ps1 +++ b/src/functions/public/Secrets/Remove-GitHubSecret.ps1 @@ -78,15 +78,38 @@ switch ($PSCmdlet.ParameterSetName) { 'ArrayInput' { foreach ($item in $InputObject) { - $params = @{ - Owner = $item.Owner - Repository = $item.Repository - Environment = $item.Environment - Name = $item.Name - Context = $item.Context + switch ($item.Scope) { + 'environment' { + $params = @{ + Owner = $item.Owner + Repository = $item.Repository + Environment = $item.Environment + Name = $item.Name + Context = $Context + } + Remove-GitHubSecretFromEnvironment @params + } + 'repository' { + $params = @{ + Owner = $item.Owner + Repository = $item.Repository + Name = $item.Name + Context = $Context + } + Remove-GitHubSecretFromRepository @params + } + 'organization' { + $params = @{ + Owner = $item.Owner + Name = $item.Name + Context = $Context + } + Remove-GitHubSecretFromOwner @params + } + default { + throw "Secret '$($item.Name)' has unsupported Scope value '$scope'." + } } - $params | Remove-HashtableEntry -NullOrEmptyValues - Remove-GitHubSecret @params } break } diff --git a/src/functions/public/Variables/Remove-GitHubVariable.ps1 b/src/functions/public/Variables/Remove-GitHubVariable.ps1 index ffe376cc3..578ad371c 100644 --- a/src/functions/public/Variables/Remove-GitHubVariable.ps1 +++ b/src/functions/public/Variables/Remove-GitHubVariable.ps1 @@ -84,15 +84,38 @@ switch ($PSCmdlet.ParameterSetName) { 'ArrayInput' { foreach ($item in $InputObject) { - $params = @{ - Owner = $item.Owner - Repository = $item.Repository - Environment = $item.Environment - Name = $item.Name - Context = $item.Context + switch ($item.Scope) { + 'environment' { + $params = @{ + Owner = $item.Owner + Repository = $item.Repository + Environment = $item.Environment + Name = $item.Name + Context = $Context + } + Remove-GitHubVariableFromEnvironment @params + } + 'repository' { + $params = @{ + Owner = $item.Owner + Repository = $item.Repository + Name = $item.Name + Context = $Context + } + Remove-GitHubVariableFromRepository @params + } + 'organization' { + $params = @{ + Owner = $item.Owner + Name = $item.Name + Context = $Context + } + Remove-GitHubVariableFromOwner @params + } + default { + throw "Variable '$($item.Name)' has unsupported Scope value '$($item.Scope)'." + } } - $params | Remove-HashtableEntry -NullOrEmptyValues - Remove-GitHubVariable @params } break } diff --git a/tests/Secrets.Tests.ps1 b/tests/Secrets.Tests.ps1 index 48bff8af7..48e06423d 100644 --- a/tests/Secrets.Tests.ps1 +++ b/tests/Secrets.Tests.ps1 @@ -215,14 +215,18 @@ Describe 'Secrets' { } } - It 'Remove-GitHubSecret' { + It 'Remove-GitHubSecret via pipeline - using pipeline' { $testSecretName = "$secretName`TestSecret*" + LogGroup 'Create secret(s) for pipeline removal' { + $create = Set-GitHubSecret @scope -Name "$secretName`TestSecretPipeline" -Value 'PipelineTestValue' + Write-Host "$($create | Format-List | Out-String)" + } LogGroup 'Before remove' { $before = Get-GitHubSecret @scope -Name $testSecretName Write-Host "$($before | Format-List | Out-String)" } LogGroup 'Remove' { - $before | Remove-GitHubSecret + Get-GitHubSecret @scope -Name $testSecretName | Remove-GitHubSecret } LogGroup 'After remove' { $after = Get-GitHubSecret @scope -Name $testSecretName @@ -231,6 +235,26 @@ Describe 'Secrets' { $after.Count | Should -Be 0 } + It 'Remove-GitHubSecret via pipeline - using variable' { + $pipelineTestSecretName = "$secretName`PipelineTest" + LogGroup 'Create test secret for pipeline removal' { + $createResult = Set-GitHubSecret @scope -Name $pipelineTestSecretName -Value 'PipelineTestValue' + Write-Host "$($createResult | Format-List | Out-String)" + } + LogGroup 'Get secret for pipeline removal' { + $secretToRemove = Get-GitHubSecret @scope -Name $pipelineTestSecretName + Write-Host "$($secretToRemove | Format-List | Out-String)" + } + LogGroup 'Remove via pipeline' { + { $secretToRemove | Remove-GitHubSecret } | Should -Not -Throw + } + LogGroup 'Verify removal' { + $after = Get-GitHubSecret @scope -Name $pipelineTestSecretName + Write-Host "$($after | Format-List | Out-String)" + $after | Should -BeNullOrEmpty + } + } + Context 'SelectedRepository' { It 'Get-GitHubSecretSelectedRepository - gets a list of selected repositories' { LogGroup "SelectedRepositories - [$orgSecretName]" { @@ -402,18 +426,42 @@ Describe 'Secrets' { } } - It 'Remove-GitHubSecret' { + It 'Remove-GitHubSecret via pipeline - using pipeline' { + LogGroup 'Create secret(s) for pipeline removal' { + $create = Set-GitHubSecret @scope -Name "$secretName`PipelineRemoval" -Value 'PipelineTestValue' + Write-Host "$($create | Format-List | Out-String)" + } $before = Get-GitHubSecret @scope -Name "*$os*" LogGroup 'Secrets - Before' { Write-Host "$($before | Format-Table | Out-String)" } - $before | Remove-GitHubSecret + Get-GitHubSecret @scope -Name "*$os*" | Remove-GitHubSecret $after = Get-GitHubSecret @scope -Name "*$os*" LogGroup 'Secrets -After' { Write-Host "$($after | Format-Table | Out-String)" } $after.Count | Should -Be 0 } + + It 'Remove-GitHubSecret via pipeline - using variable' { + $pipelineTestSecretName = "$secretName`PipelineTest" + LogGroup 'Create test secret for pipeline removal' { + $createResult = Set-GitHubSecret @scope -Name $pipelineTestSecretName -Value 'PipelineTestValue' + Write-Host "$($createResult | Format-List | Out-String)" + } + LogGroup 'Get secret for pipeline removal' { + $secretToRemove = Get-GitHubSecret @scope -Name $pipelineTestSecretName + Write-Host "$($secretToRemove | Format-List | Out-String)" + } + LogGroup 'Remove via pipeline' { + { $secretToRemove | Remove-GitHubSecret } | Should -Not -Throw + } + LogGroup 'Verify removal' { + $after = Get-GitHubSecret @scope -Name $pipelineTestSecretName + Write-Host "$($after | Format-List | Out-String)" + $after | Should -BeNullOrEmpty + } + } } Context 'Environment' -Skip:($OwnerType -in ('repository', 'enterprise')) { @@ -512,18 +560,43 @@ Describe 'Secrets' { } } - It 'Remove-GitHubSecret' { + It 'Remove-GitHubSecret via pipeline - using pipeline' { + LogGroup 'Create secret(s) for pipeline removal' { + $create = Set-GitHubSecret @scope -Name "$secretName`PipelineRemoval" -Value 'PipelineTestValue' + Write-Host "$($create | Format-List | Out-String)" + } LogGroup 'Secrets - Before' { $before = Get-GitHubSecret @scope -Name "*$os*" Write-Host "$($before | Format-Table | Out-String)" } - $before | Remove-GitHubSecret + Get-GitHubSecret @scope -Name "*$os*" | Remove-GitHubSecret LogGroup 'Secrets - After' { $after = Get-GitHubSecret @scope -Name "*$os*" Write-Host "$($after | Format-Table | Out-String)" } $after.Count | Should -Be 0 } + + It 'Remove-GitHubSecret via pipeline - using variable' { + $pipelineTestSecretName = "$secretName`PipelineTest" + LogGroup 'Create test secret for pipeline removal' { + $createResult = Set-GitHubSecret @scope -Name $pipelineTestSecretName -Value 'PipelineTestValue' + Write-Host "$($createResult | Format-List | Out-String)" + } + LogGroup 'Get secret and test whitespace handling' { + $secretToRemove = Get-GitHubSecret @scope -Name $pipelineTestSecretName + Write-Host "$($secretToRemove | Format-List | Out-String)" + Write-Host "Testing that environment secrets with valid Environment property work correctly" + } + LogGroup 'Remove via pipeline' { + { $secretToRemove | Remove-GitHubSecret } | Should -Not -Throw + } + LogGroup 'Verify removal' { + $after = Get-GitHubSecret @scope -Name $pipelineTestSecretName + Write-Host "$($after | Format-List | Out-String)" + $after | Should -BeNullOrEmpty + } + } } } } diff --git a/tests/Variables.Tests.ps1 b/tests/Variables.Tests.ps1 index 2b039ce31..0599ac435 100644 --- a/tests/Variables.Tests.ps1 +++ b/tests/Variables.Tests.ps1 @@ -240,22 +240,43 @@ Describe 'Variables' { } } - It 'Remove-GitHubVariable' { - $testVarName = "$variableName`TestVariable*" - LogGroup 'Before remove' { - $before = Get-GitHubVariable @scope -Name $testVarName - Write-Host "$($before | Format-List | Out-String)" + It 'Remove-GitHubVariable via pipeline - using pipeline' { + LogGroup 'Create variable(s) for pipeline removal' { + $create = Set-GitHubVariable @scope -Name "$variableName`PipelineRemoval" -Value 'PipelineTestValue' + Write-Host "$($create | Format-List | Out-String)" } - LogGroup 'Remove' { - $before | Remove-GitHubVariable + $before = Get-GitHubVariable @scope -Name "*$os*" + LogGroup 'Variables - Before' { + Write-Host "$($before | Format-Table | Out-String)" } - LogGroup 'After remove' { - $after = Get-GitHubVariable @scope -Name $testVarName - Write-Host "$($after | Format-List | Out-String)" + Get-GitHubVariable @scope -Name "*$os*" | Remove-GitHubVariable + $after = Get-GitHubVariable @scope -Name "*$os*" + LogGroup 'Variables -After' { + Write-Host "$($after | Format-Table | Out-String)" } $after.Count | Should -Be 0 } + It 'Remove-GitHubVariable via pipeline - using variable' { + $pipelineTestVariableName = "$variableName`PipelineTest" + LogGroup 'Create test variable for pipeline removal' { + $createResult = Set-GitHubVariable @scope -Name $pipelineTestVariableName -Value 'PipelineTestValue' + Write-Host "$($createResult | Format-List | Out-String)" + } + LogGroup 'Get variable for pipeline removal' { + $variableToRemove = Get-GitHubVariable @scope -Name $pipelineTestVariableName + Write-Host "$($variableToRemove | Format-List | Out-String)" + } + LogGroup 'Remove via pipeline' { + { $variableToRemove | Remove-GitHubVariable } | Should -Not -Throw + } + LogGroup 'Verify removal' { + $after = Get-GitHubVariable @scope -Name $pipelineTestVariableName + Write-Host "$($after | Format-List | Out-String)" + $after | Should -BeNullOrEmpty + } + } + Context 'SelectedRepository' -Tag 'Flaky' { It 'Get-GitHubVariableSelectedRepository - gets a list of selected repositories' { LogGroup "SelectedRepositories - [$orgVariableName]" { @@ -433,18 +454,42 @@ Describe 'Variables' { } } - It 'Remove-GitHubVariable' { + It 'Remove-GitHubVariable via pipeline - using pipeline' { + LogGroup 'Create variable(s) for pipeline removal' { + $create = Set-GitHubVariable @scope -Name "$variableName`PipelineRemoval" -Value 'PipelineTestValue' + Write-Host "$($create | Format-List | Out-String)" + } $before = Get-GitHubVariable @scope -Name "*$os*" LogGroup 'Variables - Before' { Write-Host "$($before | Format-Table | Out-String)" } - $before | Remove-GitHubVariable + Get-GitHubVariable @scope -Name "*$os*" | Remove-GitHubVariable $after = Get-GitHubVariable @scope -Name "*$os*" LogGroup 'Variables -After' { Write-Host "$($after | Format-Table | Out-String)" } $after.Count | Should -Be 0 } + + It 'Remove-GitHubVariable via pipeline - using variable' { + $pipelineTestVariableName = "$variableName`PipelineTest" + LogGroup 'Create test variable for pipeline removal' { + $createResult = Set-GitHubVariable @scope -Name $pipelineTestVariableName -Value 'PipelineTestValue' + Write-Host "$($createResult | Format-List | Out-String)" + } + LogGroup 'Get variable for pipeline removal' { + $variableToRemove = Get-GitHubVariable @scope -Name $pipelineTestVariableName + Write-Host "$($variableToRemove | Format-List | Out-String)" + } + LogGroup 'Remove via pipeline' { + { $variableToRemove | Remove-GitHubVariable } | Should -Not -Throw + } + LogGroup 'Verify removal' { + $after = Get-GitHubVariable @scope -Name $pipelineTestVariableName + Write-Host "$($after | Format-List | Out-String)" + $after | Should -BeNullOrEmpty + } + } } Context 'Environment' -Skip:($OwnerType -in ('repository', 'enterprise')) { @@ -560,18 +605,43 @@ Describe 'Variables' { } } - It 'Remove-GitHubVariable' { + It 'Remove-GitHubVariable via pipeline - using pipeline' { + LogGroup 'Create variable(s) for pipeline removal' { + $create = Set-GitHubVariable @scope -Name "$variableName`PipelineRemoval" -Value 'PipelineTestValue' + Write-Host "$($create | Format-List | Out-String)" + } LogGroup 'Variables - Before' { $before = Get-GitHubVariable @scope -Name "*$os*" Write-Host "$($before | Format-Table | Out-String)" } - $before | Remove-GitHubVariable + Get-GitHubVariable @scope -Name "*$os*" | Remove-GitHubVariable LogGroup 'Variables - After' { $after = Get-GitHubVariable @scope -Name "*$os*" Write-Host "$($after | Format-Table | Out-String)" } $after.Count | Should -Be 0 } + + It 'Remove-GitHubVariable via pipeline - using variable' { + $pipelineTestVariableName = "$variableName`PipelineTest" + LogGroup 'Create test variable for pipeline removal' { + $createResult = Set-GitHubVariable @scope -Name $pipelineTestVariableName -Value 'PipelineTestValue' + Write-Host "$($createResult | Format-List | Out-String)" + } + LogGroup 'Get variable and test whitespace handling' { + $variableToRemove = Get-GitHubVariable @scope -Name $pipelineTestVariableName + Write-Host "$($variableToRemove | Format-List | Out-String)" + Write-Host "Testing that environment variables with valid Environment property work correctly" + } + LogGroup 'Remove via pipeline' { + { $variableToRemove | Remove-GitHubVariable } | Should -Not -Throw + } + LogGroup 'Verify removal' { + $after = Get-GitHubVariable @scope -Name $pipelineTestVariableName + Write-Host "$($after | Format-List | Out-String)" + $after | Should -BeNullOrEmpty + } + } } } } From fff966da86a2cf4175dd7810b27b091ed0a4d1fd Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Sep 2025 15:17:24 +0200 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=A9=B9=20[Patch]:=20Add=20Fine-Graine?= =?UTF-8?q?d=20Permissions=20Data=20for=20GitHub=20PowerShell=20Module=20(?= =?UTF-8?q?#501)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR implements a comprehensive fine-grained permissions data infrastructure for the GitHub PowerShell module, enabling detection of GitHub App installations that may be missing newly added permissions. - Fixes #485 ## What's New ### GitHubPermission Class Added a new public `GitHubPermission` class with the following properties: - **Name** - Programmatic permission name (e.g., `contents`, `issues`) - **DisplayName** - Human-friendly name (e.g., "Contents", "Issues") - **Description** - Brief description of what access the permission grants - **URL** - Link to relevant GitHub documentation - **Options** - Available access levels (`read`, `write`, `admin`) - **Type** - Permission type (`Fine-grained`, `Classic`) - **Scope** - Application scope (`Repository`, `Organization`, `User`, `Enterprise`) ### Comprehensive Permissions Database Added 90 fine-grained permissions covering all major GitHub permission categories: - **33 Repository permissions** - actions, contents, issues, pull_requests, secrets, etc. - **33 Organization permissions** - members, administration, organization_secrets, etc. - **18 User permissions** - profile, followers, git_ssh_keys, etc. - **6 Enterprise permissions** - custom properties, organization installation, etc. ### Get-GitHubPermissionDefinition Function New public function to query the permissions database with advanced filtering: ```powershell # Get all permissions Get-GitHubPermissionDefinition # Filter by scope Get-GitHubPermissionDefinition -Scope Repository # Combined filtering Get-GitHubPermissionDefinition -Type Fine-grained -Scope Organization # Find specific permissions Get-GitHubPermissionDefinition -Name 'contents' ``` ### Argument Completers Added argument completers for `Get-GitHubPermissionDefinition` parameters to improve user experience: - **Name** - Tab completion for available permission names (actions, contents, issues, etc.) - **DisplayName** - Tab completion for available permission display names (Actions, Dependabot alerts, etc.) - **Type** - Tab completion for available permission types (Fine-grained) - **Scope** - Tab completion for available scopes (Repository, Organization, User, Enterprise) ## Use Cases This infrastructure enables several key scenarios: 1. **Permission Validation** - Compare GitHub App installations against the complete permissions list 2. **Installation Health Checks** - Detect apps missing newly added permissions 3. **Documentation** - Provide users with comprehensive permission reference 4. **Automation** - Build tools that ensure installations stay current with permission requirements 5. **Enhanced User Experience** - Tab completion for parameter values improves usability ## Implementation Details - **File path permissions excluded** - These are handled differently by the GitHub API (appear under `FilePaths` property rather than as named permissions) - **Maintainable structure** - Easy to update when GitHub adds new permissions - **Performance optimized** - Efficient filtering and lookup operations - **Comprehensive testing** - Full test coverage for all functionality - **Argument completion** - Improves user experience with tab completion support ## Example Usage ```powershell # Check what repository permissions are available (with tab completion) $repoPerms = Get-GitHubPermissionDefinition -Scope Write-Host "Repository permissions: $($repoPerms.Count)" # Get details about the contents permission (with tab completion) $contents = Get-GitHubPermissionDefinition -Name cont Write-Host "$($contents.DisplayName): $($contents.Description)" Write-Host "Available options: $($contents.Options -join ', ')" ``` This provides the foundation for building automated permission management tools and ensuring GitHub App installations remain up-to-date with the latest permission requirements. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: MariusStorhaug <17722253+MariusStorhaug@users.noreply.github.com> Co-authored-by: Marius Storhaug --- .../public/GitHubPermissionDefinition.ps1 | 46 + .../GitHubPermissionDefinition.Format.ps1xml | 46 + .../Get-GitHubPermissionDefinition.ps1 | 104 ++ .../public/Permission/completers.ps1 | 35 + src/variables/private/GitHub.ps1 | 1180 +++++++++++++++++ tests/Permissions.Tests.ps1 | 211 +++ 6 files changed, 1622 insertions(+) create mode 100644 src/classes/public/GitHubPermissionDefinition.ps1 create mode 100644 src/formats/GitHubPermissionDefinition.Format.ps1xml create mode 100644 src/functions/public/Permission/Get-GitHubPermissionDefinition.ps1 create mode 100644 src/functions/public/Permission/completers.ps1 create mode 100644 tests/Permissions.Tests.ps1 diff --git a/src/classes/public/GitHubPermissionDefinition.ps1 b/src/classes/public/GitHubPermissionDefinition.ps1 new file mode 100644 index 000000000..93ff6643f --- /dev/null +++ b/src/classes/public/GitHubPermissionDefinition.ps1 @@ -0,0 +1,46 @@ +class GitHubPermissionDefinition { + # The programmatic name of the permission as returned by the GitHub API + [string] $Name + + # The human-friendly name of the permission as shown in the GitHub UI + [string] $DisplayName + + # A brief description of what access the permission grants + [string] $Description + + # A link to the relevant documentation or GitHub UI page + [uri] $URL + + # The levels of access that can be granted for this permission + [string[]] $Options + + # The type of permission (Fine-grained, Classic) + [string] $Type + + # The scope at which this permission applies (Repository, Organization, User, Enterprise) + [string] $Scope + + GitHubPermissionDefinition() {} + + GitHubPermissionDefinition( + [string]$Name, + [string]$DisplayName, + [string]$Description, + [string]$URL, + [string[]]$Options, + [string]$Type, + [string]$Scope + ) { + $this.Name = $Name + $this.DisplayName = $DisplayName + $this.Description = $Description + $this.URL = [uri]$URL + $this.Options = $Options + $this.Type = $Type + $this.Scope = $Scope + } + + [string] ToString() { + return $this.Name + } +} diff --git a/src/formats/GitHubPermissionDefinition.Format.ps1xml b/src/formats/GitHubPermissionDefinition.Format.ps1xml new file mode 100644 index 000000000..756b42381 --- /dev/null +++ b/src/formats/GitHubPermissionDefinition.Format.ps1xml @@ -0,0 +1,46 @@ + + + + + GitHubPermissionDefinitionTable + + GitHubPermissionDefinition + + + + + + + + + + + + + + + + + + Scope + + + + if ($Host.UI.SupportsVirtualTerminal -and + ($env:GITHUB_ACTIONS -ne 'true') -and $_.Url) { + $PSStyle.FormatHyperlink($_.DisplayName, $_.Url) + } else { + $_.DisplayName + } + + + + Description + + + + + + + + diff --git a/src/functions/public/Permission/Get-GitHubPermissionDefinition.ps1 b/src/functions/public/Permission/Get-GitHubPermissionDefinition.ps1 new file mode 100644 index 000000000..0019adf59 --- /dev/null +++ b/src/functions/public/Permission/Get-GitHubPermissionDefinition.ps1 @@ -0,0 +1,104 @@ +function Get-GitHubPermissionDefinition { + <# + .SYNOPSIS + Retrieves GitHub permission definitions + + .DESCRIPTION + Gets the list of GitHub permission definitions from the module's internal data store. + This includes fine-grained permissions for repositories, organizations, and user accounts. + The function supports filtering by permission type and scope to help you find specific permissions. + + File path-specific permissions are excluded from this list as they are handled differently + by the GitHub API (they appear under the FilePaths property in installation data rather + than as named permissions). + + .EXAMPLE + Get-GitHubPermissionDefinition + + Gets all permission definitions. + + .EXAMPLE + Get-GitHubPermissionDefinition -Type Fine-grained + + Gets all fine-grained permission definitions. + + .EXAMPLE + Get-GitHubPermissionDefinition -Scope Repository + + Gets all permission definitions that apply to repository scope. + + .EXAMPLE + Get-GitHubPermissionDefinition -Type Fine-grained -Scope Organization + + Gets all fine-grained permission definitions that apply to organization scope. + + .EXAMPLE + Get-GitHubPermissionDefinition -Name contents + + Gets the specific permission definition for 'contents' permission. + + .NOTES + This function provides access to a curated list of GitHub permission definitions maintained within the module. + The data includes permission names, display names, descriptions, available options, and scopes. + + File path permissions are excluded from this list as they are handled differently by the GitHub API. + These permissions are user-specified paths with read/write access that appear in the FilePaths + property of GitHub App installation data, not as standard named permissions. + + .LINK + https://psmodule.io/GitHub/Functions/Permission/Get-GitHubPermissionDefinition + #> + [OutputType([GitHubPermissionDefinition[]])] + [CmdletBinding()] + param( + # Filter by permission name (supports multiple values & wildcards) + [Parameter()] + [string[]] $Name = '*', + + # Filter by permission display name (supports multiple values & wildcards) + [Parameter()] + [string[]] $DisplayName = '*', + + # Filter by permission type (supports multiple values & wildcards) + [Parameter()] + [string[]] $Type = '*', + + # Filter by permission scope (supports multiple values & wildcards) + [Parameter()] + [string[]] $Scope = '*' + ) + + begin { + $stackPath = Get-PSCallStackPath + Write-Debug "[$stackPath] - Start" + } + + process { + try { + [scriptblock]$test = { + param( + [Parameter(Mandatory)][string] $Value, + [Parameter(Mandatory)][string[]] $Patterns + ) + foreach ($p in $Patterns) { + if ($Value -like $p) { return $true } + } + return $false + } + + $script:GitHub.Permissions | Where-Object { + (& $test -Value $_.Name -Patterns $Name) -and + (& $test -Value $_.DisplayName -Patterns $DisplayName) -and + (& $test -Value $_.Type -Patterns $Type) -and + (& $test -Value $_.Scope -Patterns $Scope) + } + } catch { + Write-Error "Failed to retrieve GitHub permission definitions: $($_.Exception.Message)" + throw + } + } + + end { + Write-Debug "[$stackPath] - End" + } +} diff --git a/src/functions/public/Permission/completers.ps1 b/src/functions/public/Permission/completers.ps1 new file mode 100644 index 000000000..139a72e5f --- /dev/null +++ b/src/functions/public/Permission/completers.ps1 @@ -0,0 +1,35 @@ +Register-ArgumentCompleter -CommandName Get-GitHubPermissionDefinition -ParameterName Name -ScriptBlock { + param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) + $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters + + $script:GitHub.Permissions.Name | Sort-Object -Unique | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } +} + +Register-ArgumentCompleter -CommandName Get-GitHubPermissionDefinition -ParameterName DisplayName -ScriptBlock { + param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) + $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters + + $script:GitHub.Permissions.DisplayName | Sort-Object -Unique | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } +} + +Register-ArgumentCompleter -CommandName Get-GitHubPermissionDefinition -ParameterName Type -ScriptBlock { + param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) + $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters + + $script:GitHub.Permissions.Type | Sort-Object -Unique | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } +} + +Register-ArgumentCompleter -CommandName Get-GitHubPermissionDefinition -ParameterName Scope -ScriptBlock { + param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) + $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters + + $script:GitHub.Permissions.Scope | Sort-Object -Unique | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } +} diff --git a/src/variables/private/GitHub.ps1 b/src/variables/private/GitHub.ps1 index 3891118b3..917d0be14 100644 --- a/src/variables/private/GitHub.ps1 +++ b/src/variables/private/GitHub.ps1 @@ -23,4 +23,1184 @@ $script:GitHub = [pscustomobject]@{ Config = $null Event = $null Runner = $null + Permissions = @( + # ------------------------------ + # Repository Fine-Grained Permission Definitions + # ------------------------------ + [GitHubPermissionDefinition]::new( + 'actions', + 'Actions', + 'Workflows, workflow runs and artifacts.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-actions', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'administration', + 'Administration', + 'Repository creation, deletion, settings, teams, and collaborators.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-administration', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'attestations', + 'Attestations', + 'Create and retrieve attestations for a repository.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-attestations', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'checks', + 'Checks', + 'Checks on code.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-checks', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'security_events', + 'Code scanning alerts', + 'View and manage code scanning alerts.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-code-scanning-alerts', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'codespaces', + 'Codespaces', + 'Create, edit, delete and list Codespaces.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-codespaces', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'codespaces_lifecycle_admin', + 'Codespaces lifecycle admin', + 'Manage the lifecycle of Codespaces, including starting and stopping.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-codespaces-lifecycle-admin', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'codespaces_metadata', + 'Codespaces metadata', + 'Access Codespaces metadata including the devcontainers and machine type.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-codespaces-metadata', + @( + 'read' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'codespaces_secrets', + 'Codespaces secrets', + 'Restrict Codespaces user secrets modifications to specific repositories.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-codespaces-secrets', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'statuses', + 'Commit statuses', + 'Commit statuses.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-commit-statuses', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'contents', + 'Contents', + 'Repository contents, commits, branches, downloads, releases, and merges.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-contents', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'repository_custom_properties', + 'Custom properties', + 'Read and write repository custom properties values at the repository level, when allowed by the property.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-custom-properties', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'vulnerability_alerts', + 'Dependabot alerts', + 'Retrieve Dependabot alerts.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-dependabot-alerts', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'dependabot_secrets', + 'Dependabot secrets', + 'Manage Dependabot repository secrets.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-dependabot-secrets', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'deployments', + 'Deployments', + 'Deployments and deployment statuses.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-deployments', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'discussions', + 'Discussions', + 'Discussions and related comments and labels.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-discussions', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'environments', + 'Environments', + 'Manage repository environments.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-environments', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'issues', + 'Issues', + 'Issues and related comments, assignees, labels, and milestones.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-issues', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'merge_queues', + 'Merge queues', + "Manage a repository's merge queues", + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-merge-queues', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( #Mandatory + 'metadata', + 'Metadata', + 'Search repositories, list collaborators, and access repository metadata.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-metadata', + @( + 'read' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'packages', + 'Packages', + 'Packages published to the GitHub Package Platform.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-packages', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'pages', + 'Pages', + 'Retrieve Pages statuses, configuration, and builds, as well as create new builds.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-pages', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'repository_projects', + 'Projects', + 'Manage classic projects within a repository.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-projects', + @( + 'read', + 'write', + 'admin' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'pull_requests', + 'Pull requests', + 'Pull requests and related comments, assignees, labels, milestones, and merges.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-pull-requests', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'repository_advisories', + 'Repository security advisories', + 'View and manage repository security advisories.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-repository-security-advisories', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'repo_secret_scanning_dismissal_requests', + 'Secret scanning alert dismissal requests', + 'View and manage secret scanning alert dismissal requests', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-secret-scanning-alert-dismissal-requests', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'secret_scanning_alerts', + 'Secret scanning alerts', + 'View and manage secret scanning alerts.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-secret-scanning-alerts', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'secret_scanning_bypass_requests', + 'Secret scanning push protection bypass requests', + 'Review and manage repository secret scanning push protection bypass requests.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-secret-scanning-push-protection-bypass-requests', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'secrets', + 'Secrets', + 'Manage Actions repository secrets.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-secrets', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'single_file', + 'Single file', + 'Manage just a single file.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-single-file', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'actions_variables', + 'Variables', + 'Manage Actions repository variables.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-variables', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'repository_hooks', + 'Webhooks', + 'Manage the post-receive hooks for a repository.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-webhooks', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + [GitHubPermissionDefinition]::new( + 'workflows', + 'Workflows', + 'Update GitHub Action workflow files.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#repository-permissions-for-workflows', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Repository' + ), + + # ------------------------------ + # Organization Fine-Grained Permission Definitions + # ------------------------------ + [GitHubPermissionDefinition]::new( + 'organization_api_insights', + 'API Insights', + 'View statistics on how the API is being used for an organization.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-api-insights', + @( + 'read' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_administration', + 'Administration', + 'Manage access to an organization.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-administration', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_user_blocking', + 'Blocking users', + 'View and manage users blocked by the organization.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-blocking-users', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_campaigns', + 'Campaigns', + 'Manage campaigns.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-campaigns', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_custom_org_roles', + 'Custom organization roles', + 'Create, edit, delete and list custom organization roles. View system organization roles.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-custom-organization-roles', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_custom_properties', + 'Custom properties', + 'Read and write repository custom properties values and administer definitions at the organization level.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-custom-properties', + @( + 'read', + 'write', + 'admin' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_custom_roles', + 'Custom repository roles', + 'Create, edit, delete and list custom repository roles.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-custom-repository-roles', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_events', + 'Events', + 'View events triggered by an activity in an organization.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-events', + @( + 'read' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_copilot_seat_management', + 'GitHub Copilot Business', + 'Manage Copilot Business seats and settings', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-github-copilot-business', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'issue_fields', + 'Issue Fields', + 'Manage issue fields for an organization.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-issue-fields', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'issue_types', + 'Issue Types', + 'Manage issue types for an organization.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-issue-types', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_knowledge_bases', + 'Knowledge bases', + 'View and manage knowledge bases for an organization.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-knowledge-bases', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'members', + 'Members', + 'Organization members and teams.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-members', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_models', + 'Models', + 'Manage model access for an organization.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-models', + @( + 'read' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_network_configurations', + 'Network configurations', + 'View and manage hosted compute network configurations available to an organization.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-network-configurations', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_announcement_banners', + 'Organization announcement banners', + 'View and modify announcement banners for an organization.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-organization-announcement-banners', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_secret_scanning_bypass_requests', + 'Organization bypass requests for secret scanning', + 'Review and manage secret scanning push protection bypass requests.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-organization-bypass-requests-for-secret-scanning', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_codespaces', + 'Organization codespaces', + 'Manage Codespaces for an organization.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-organization-codespaces', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_codespaces_secrets', + 'Organization codespaces secrets', + 'Manage Codespaces Secrets for an organization.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-organization-codespaces-secrets', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_codespaces_settings', + 'Organization codespaces settings', + 'Manage Codespaces settings for an organization.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-organization-codespaces-settings', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_dependabot_secrets', + 'Organization dependabot secrets', + 'Manage Dependabot organization secrets.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-organization-dependabot-secrets', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_code_scanning_dismissal_requests', + 'Organization dismissal requests for code scanning', + 'Review and manage code scanning alert dismissal requests.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-organization-dismissal-requests-for-code-scanning', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_private_registries', + 'Organization private registries', + 'Manage private registries for an organization.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-organization-private-registries', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_personal_access_token_requests', + 'Personal access token requests', + 'Manage personal access token requests from organization members.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-personal-access-token-requests', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_personal_access_tokens', + 'Personal access tokens', + 'View and revoke personal access tokens that have been granted access to an organization.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-personal-access-tokens', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_plan', + 'Plan', + "View an organization's plan.", + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-plan', + @( + 'read' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_projects', + 'Projects', + 'Manage projects for an organization.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-projects', + @( + 'read', + 'write', + 'admin' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'secret_scanning_dismissal_requests', + 'Secret scanning alert dismissal requests', + 'Review and manage secret scanning alert dismissal requests', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-secret-scanning-alert-dismissal-requests', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_secrets', + 'Secrets', + 'Manage Actions organization secrets.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-secrets', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_self_hosted_runners', + 'Self-hosted runners', + 'View and manage Actions self-hosted runners available to an organization.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-self-hosted-runners', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'team_discussions', + 'Team discussions', + 'Manage team discussions and related comments.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-team-discussions', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_actions_variables', + 'Variables', + 'Manage Actions organization variables.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-variables', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + [GitHubPermissionDefinition]::new( + 'organization_hooks', + 'Webhooks', + 'Manage the post-receive hooks for an organization.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#organization-permissions-for-webhooks', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Organization' + ), + + # ------------------------------ + # User (Account) Fine-Grained Permission Definitions + # ------------------------------ + [GitHubPermissionDefinition]::new( + 'blocking', + 'Block another user', + 'View and manage users blocked by the user.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#user-permissions-for-block-another-user', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'User' + ), + [GitHubPermissionDefinition]::new( + 'codespaces_user_secrets', + 'Codespaces user secrets', + 'Manage Codespaces user secrets.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#user-permissions-for-codespaces-user-secrets', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'User' + ), + [GitHubPermissionDefinition]::new( + 'copilot_messages', + 'Copilot Chat', + 'This application will receive your GitHub ID, your GitHub Copilot Chat session messages ' + + '(not including messages sent to another application), and timestamps of provided GitHub Copilot ' + + 'Chat session messages. This permission must be enabled for Copilot Extensions.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#user-permissions-for-copilot-chat', + @( + 'read' + ), + 'Fine-grained', + 'User' + ), + [GitHubPermissionDefinition]::new( + 'copilot_editor_context', + 'Copilot Editor Context', + 'This application will receive bits of Editor Context (e.g. currently opened file) whenever you send it a message through Copilot Chat.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#user-permissions-for-copilot-editor-context', + @( + 'read' + ), + 'Fine-grained', + 'User' + ), + [GitHubPermissionDefinition]::new( + 'emails', + 'Email addresses', + "Manage a user's email addresses.", + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#user-permissions-for-email-addresses', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'User' + ), + [GitHubPermissionDefinition]::new( + 'user_events', + 'Events', + "View events triggered by a user's activity.", + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#user-permissions-for-events', + @( + 'read' + ), + 'Fine-grained', + 'User' + ), + [GitHubPermissionDefinition]::new( + 'followers', + 'Followers', + "A user's followers", + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#user-permissions-for-followers', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'User' + ), + [GitHubPermissionDefinition]::new( + 'gpg_keys', + 'GPG keys', + "View and manage a user's GPG keys.", + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#user-permissions-for-gpg-keys', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'User' + ), + [GitHubPermissionDefinition]::new( + 'gists', + 'Gists', + "Create and modify a user's gists and comments.", + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#user-permissions-for-gists', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'User' + ), + [GitHubPermissionDefinition]::new( + 'keys', + 'Git SSH keys', + 'Git SSH keys', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#user-permissions-for-git-ssh-keys', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'User' + ), + [GitHubPermissionDefinition]::new( + 'interaction_limits', + 'Interaction limits', + 'Interaction limits on repositories', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#user-permissions-for-interaction-limits', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'User' + ), + [GitHubPermissionDefinition]::new( + 'knowledge_bases', + 'Knowledge bases', + 'View knowledge bases for a user.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#user-permissions-for-knowledge-bases', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'User' + ), + [GitHubPermissionDefinition]::new( + 'user_models', + 'Models', + 'Allows access to GitHub Models.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#user-permissions-for-models', + @( + 'read' + ), + 'Fine-grained', + 'User' + ), + [GitHubPermissionDefinition]::new( + 'plan', + 'Plan', + "View a user's plan.", + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#user-permissions-for-plan', + @( + 'read' + ), + 'Fine-grained', + 'User' + ), + [GitHubPermissionDefinition]::new( + 'profile', + 'Profile', + "Manage a user's profile settings.", + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#user-permissions-for-profile', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'User' + ), + [GitHubPermissionDefinition]::new( + 'git_signing_ssh_public_keys', + 'SSH signing keys', + "View and manage a user's SSH signing keys.", + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#user-permissions-for-ssh-signing-keys', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'User' + ), + [GitHubPermissionDefinition]::new( + 'starring', + 'Starring', + 'List and manage repositories a user is starring.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#user-permissions-for-starring', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'User' + ), + [GitHubPermissionDefinition]::new( + 'watching', + 'Watching', + 'List and change repositories a user is subscribed to.', + 'https://docs.github.com/rest/overview/permissions-required-for-github-apps' + + '#user-permissions-for-watching', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'User' + ), + + # ------------------------------ + # Enterprise Fine-Grained Permission Definitions + # ------------------------------ + [GitHubPermissionDefinition]::new( + 'enterprise_custom_properties', + 'Custom properties', + 'View repository custom properties and administer definitions at the enterprise level.', + 'https://docs.github.com/enterprise-cloud@latest/rest/overview/permissions-required-for-github-apps' + + '#enterprise-permissions-for-custom-properties', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Enterprise' + ), + [GitHubPermissionDefinition]::new( + 'enterprise_organization_installation_repositories', + 'Enterprise organization installation repositories', + 'Manage repository access of GitHub Apps on Enterprise-owned organizations', + 'https://docs.github.com/enterprise-cloud@latest/rest/overview/permissions-required-for-github-apps' + + '#enterprise-permissions-for-enterprise-organization-installation-repositories', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Enterprise' + ), + [GitHubPermissionDefinition]::new( + 'enterprise_organization_installations', + 'Enterprise organization installations', + 'Manage installation of GitHub Apps on Enterprise-owned organizations', + 'https://docs.github.com/enterprise-cloud@latest/rest/overview/permissions-required-for-github-apps' + + '#enterprise-permissions-for-enterprise-organization-installations', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Enterprise' + ), + [GitHubPermissionDefinition]::new( + 'enterprise_organizations', + 'Enterprise organizations', + 'Create and remove enterprise organizations', + 'https://docs.github.com/enterprise-cloud@latest/rest/overview/permissions-required-for-github-apps' + + '#enterprise-permissions-for-enterprise-organizations', + @( + 'write' + ), + 'Fine-grained', + 'Enterprise' + ), + [GitHubPermissionDefinition]::new( + 'enterprise_people', + 'Enterprise people', + 'Manage user access to the enterprise', + 'https://docs.github.com/enterprise-cloud@latest/rest/overview/permissions-required-for-github-apps' + + '#enterprise-permissions-for-enterprise-people', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Enterprise' + ), + [GitHubPermissionDefinition]::new( + 'enterprise_sso', + 'Enterprise single sign-on', + 'View and manage enterprise single sign-on configuration', + 'https://docs.github.com/enterprise-cloud@latest/rest/overview/permissions-required-for-github-apps' + + '#enterprise-permissions-for-enterprise-single-sign-on', + @( + 'read', + 'write' + ), + 'Fine-grained', + 'Enterprise' + ) + ) } diff --git a/tests/Permissions.Tests.ps1 b/tests/Permissions.Tests.ps1 new file mode 100644 index 000000000..043358796 --- /dev/null +++ b/tests/Permissions.Tests.ps1 @@ -0,0 +1,211 @@ +#Requires -Modules @{ ModuleName = 'Pester'; RequiredVersion = '5.7.1' } + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Pester grouping syntax: known issue.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingConvertToSecureStringWithPlainText', '', + Justification = 'Used to create a secure string for testing.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingWriteHost', '', + Justification = 'Log outputs to GitHub Actions logs.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidLongLines', '', + Justification = 'Long test descriptions and skip switches' +)] +[CmdletBinding()] +param() + +Describe 'Permissions' { + $authCases = . "$PSScriptRoot/Data/AuthCases.ps1" + + Context 'As using on ' -ForEach $authCases { + BeforeAll { + $context = Connect-GitHubAccount @connectParams -PassThru -Silent + LogGroup 'Context' { + Write-Host ($context | Format-List | Out-String) + } + } + + AfterAll { + Get-GitHubContext -ListAvailable | Disconnect-GitHubAccount -Silent + Write-Host ('-' * 60) + } + + Context 'For Apps' -Skip:($AuthType -ne 'APP') { + BeforeAll { + $installationContext = Connect-GitHubApp @connectAppParams -PassThru -Default -Silent + LogGroup 'Context - Installation' { + Write-Host ($installationContext | Format-List | Out-String) + } + } + + It 'App context should have Permissions property populated' -Skip:($AuthType -ne 'APP') { + $installationContext.Permissions | Should -Not -BeNullOrEmpty + $installationContext.Permissions | Should -BeOfType [pscustomobject] + } + + It 'All app installation permissions should exist in permission catalog and be valid options' -Skip:($AuthType -ne 'APP') { + $catalog = Get-GitHubPermissionDefinition + $catalogNames = $catalog.Name + + # Flatten context permission hashtable/object into name/value pairs (value is access level like read/write/admin) + $granted = @() + $installationContext.Permissions.PSObject.Properties | ForEach-Object { + if ($_.Name -eq 'metadata') { return } # metadata is mandatory; still in catalog but just proceed normally + $granted += [pscustomobject]@{ Name = $_.Name; Level = [string]$_.Value } + } + + # Unknown permissions (present in context but not in catalog) + $unknown = $granted | Where-Object { $_.Name -notin $catalogNames } + if ($unknown) { + throw "Unknown permission(s) detected in app installation: $($unknown.Name -join ', ')" + } + + # For each granted permission ensure level is one of the catalog options + foreach ($g in $granted) { + $def = $catalog | Where-Object Name -EQ $g.Name + $def | Should -Not -BeNullOrEmpty + $def.Options | Should -Contain $g.Level + } + } + + It 'Permission catalog should contain all permissions granted to the app installation' -Skip:($AuthType -ne 'APP') { + $catalog = Get-GitHubPermissionDefinition + $missing = @() + $installationContext.Permissions.PSObject.Properties | ForEach-Object { + if ($_.Name -notin $catalog.Name) { + $missing += $_.Name + } + } + if ($missing.Count -gt 0) { + throw "Missing permission definitions for: $($missing -join ', ')" + } + } + } + } + + Context 'Get-GitHubPermissionDefinition' { + It 'Should return all permission definitions when called without parameters' { + $result = Get-GitHubPermissionDefinition + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType [GitHubPermissionDefinition] + ($result | Measure-Object).Count | Should -BeGreaterThan 0 + } + + It 'Should return only Fine-grained permissions when filtered by Type' { + $result = Get-GitHubPermissionDefinition -Type Fine-grained + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType [GitHubPermissionDefinition] + $result | ForEach-Object { $_.Type | Should -Be 'Fine-grained' } + } + + It 'Should return only Repository permissions when filtered by Scope' { + $result = Get-GitHubPermissionDefinition -Scope Repository + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType [GitHubPermissionDefinition] + $result | ForEach-Object { $_.Scope | Should -Be 'Repository' } + } + + It 'Should return only Organization permissions when filtered by Scope' { + $result = Get-GitHubPermissionDefinition -Scope Organization + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType [GitHubPermissionDefinition] + $result | ForEach-Object { $_.Scope | Should -Be 'Organization' } + } + + It 'Should return only User permissions when filtered by Scope' { + $result = Get-GitHubPermissionDefinition -Scope User + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType [GitHubPermissionDefinition] + $result | ForEach-Object { $_.Scope | Should -Be 'User' } + } + + It 'Should filter by both Type and Scope when both are specified' { + $result = Get-GitHubPermissionDefinition -Type Fine-grained -Scope Repository + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType [GitHubPermissionDefinition] + $result | ForEach-Object { + $_.Type | Should -Be 'Fine-grained' + $_.Scope | Should -Be 'Repository' + } + } + + It 'Should include expected properties for each permission' { + $result = Get-GitHubPermissionDefinition | Select-Object -First 1 + $result.Name | Should -Not -BeNullOrEmpty + $result.DisplayName | Should -Not -BeNullOrEmpty + $result.Description | Should -Not -BeNullOrEmpty + $result.URL | Should -Not -BeNullOrEmpty + $result.Options | Should -Not -BeNullOrEmpty + $result.Type | Should -Not -BeNullOrEmpty + $result.Scope | Should -Not -BeNullOrEmpty + } + + It 'Should include the contents permission for repositories' { + $result = Get-GitHubPermissionDefinition | Where-Object { $_.Name -eq 'contents' } + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'contents' + $result.DisplayName | Should -Be 'Contents' + $result.Scope | Should -Be 'Repository' + $result.Type | Should -Be 'Fine-grained' + $result.Options | Should -Contain 'read' + $result.Options | Should -Contain 'write' + } + + It 'Should include the members permission for organizations' { + $result = Get-GitHubPermissionDefinition | Where-Object { $_.Name -eq 'members' } + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'members' + $result.DisplayName | Should -Be 'Members' + $result.Scope | Should -Be 'Organization' + $result.Type | Should -Be 'Fine-grained' + $result.Options | Should -Contain 'read' + $result.Options | Should -Contain 'write' + } + + It 'Should include profile permission for users' { + $result = Get-GitHubPermissionDefinition | Where-Object { $_.Name -eq 'profile' } + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'profile' + $result.DisplayName | Should -Be 'Profile' + $result.Scope | Should -Be 'User' + $result.Type | Should -Be 'Fine-grained' + $result.Options | Should -Contain 'write' + } + } + + Context 'GitHubPermission Class' { + BeforeAll { + $permission = [GitHubPermissionDefinition]@{ + Name = 'test' + DisplayName = 'Test Permission' + Description = 'A test permission' + URL = 'https://docs.github.com/test' + Options = @('read', 'write') + Type = 'Fine-grained' + Scope = 'Repository' + } + } + + It 'Should create a GitHubPermission object with all properties' { + $permission | Should -Not -BeNullOrEmpty + $permission.Name | Should -Be 'test' + $permission.DisplayName | Should -Be 'Test Permission' + $permission.Description | Should -Be 'A test permission' + $permission.URL | Should -Be 'https://docs.github.com/test' + $permission.Options | Should -Contain 'read' + $permission.Options | Should -Contain 'write' + $permission.Type | Should -Be 'Fine-grained' + $permission.Scope | Should -Be 'Repository' + } + + It 'Should have a meaningful ToString() method' { + $result = $permission.ToString() + $result | Should -Be 'test' + } + } +} From 4d83c75bffb579d04aae49aa4853958310cca12e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Sep 2025 22:03:38 +0200 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=A9=B9=20[Patch]:=20Add=20OwnerUrl=20?= =?UTF-8?q?and=20RepositoryUrl=20properties=20to=20GitHubEnvironment=20cla?= =?UTF-8?q?ss=20and=20enable=20hyperlinks=20in=20format=20display=20(#502)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes Made Added two new properties to the `GitHubEnvironment` class: - `OwnerUrl` - URL to the owner/organization profile (e.g., `https://github.com/octocat`) - `RepositoryUrl` - URL to the repository (e.g., `https://github.com/octocat/Hello-World`) These properties are automatically populated in the constructor using the existing `Context.HostName` pattern, ensuring compatibility with both GitHub.com and GitHub Enterprise instances. Updated the URL property comments to include examples following the same format used in other classes: - `OwnerUrl` - Example: `https://github.com/octocat` - `RepositoryUrl` - Example: `https://github.com/octocat/Hello-World` - `Url` - Example: `https://github.com/octocat/Hello-World/settings/environments/123/edit` Updated the `GitHubEnvironment.Format.ps1xml` file to add hyperlinks to Owner and Repository columns when the host supports virtual terminal (and not running in GitHub Actions), following the same pattern used in other format files. ## Example Usage ```powershell $environment = Get-GitHubEnvironment -Owner 'octocat' -Repository 'Hello-World' -Name 'production' # New properties provide direct access to related URLs Write-Host "Owner URL: $($environment.OwnerUrl)" # https://github.com/octocat Write-Host "Repository URL: $($environment.RepositoryUrl)" # https://github.com/octocat/Hello-World Write-Host "Environment URL: $($environment.Url)" # https://github.com/octocat/Hello-World/settings/environments/123/edit ``` Now when displaying GitHubEnvironment objects in a table format, the Owner and Repository names will be clickable hyperlinks (when terminal supports it) that navigate to their respective GitHub pages. ## Benefits - **Consistency**: Follows the same URL pattern and documentation format established by other classes like `GitHubRepository` and `GitHubOwner` - **Enhanced UX**: Owner and Repository names are now clickable links in terminal environments that support hyperlinks - **Convenience**: Users no longer need to manually construct organization or repository URLs - **Enterprise Support**: Works seamlessly with custom GitHub Enterprise hostnames - **Non-breaking**: Purely additive changes that don't affect existing functionality Fixes #455. --- 💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: MariusStorhaug <17722253+MariusStorhaug@users.noreply.github.com> --- .../public/Environment/GitHubEnvironment.ps1 | 11 +++++++++++ src/formats/GitHubEnvironment.Format.ps1xml | 18 ++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/classes/public/Environment/GitHubEnvironment.ps1 b/src/classes/public/Environment/GitHubEnvironment.ps1 index 799addd97..9fb960fe1 100644 --- a/src/classes/public/Environment/GitHubEnvironment.ps1 +++ b/src/classes/public/Environment/GitHubEnvironment.ps1 @@ -8,7 +8,16 @@ # The owner of the environment. [string] $Owner + # URL to the owner/organization profile. + # Example: https://github.com/octocat + [string] $OwnerUrl + + # URL to the repository. + # Example: https://github.com/octocat/Hello-World + [string] $RepositoryUrl + # URL of the environment. + # Example: https://github.com/octocat/Hello-World/settings/environments/123/edit [string] $Url # The date and time the environment was created. @@ -34,6 +43,8 @@ $this.Name = $Object.name $this.Owner = $Owner $this.Repository = $Repository + $this.OwnerUrl = "https://$($Context.HostName)/$Owner" + $this.RepositoryUrl = "https://$($Context.HostName)/$Owner/$Repository" $this.Url = "https://$($Context.HostName)/$Owner/$Repository/settings/environments/$($Object.id)/edit" $this.CreatedAt = $Object.created_at $this.UpdatedAt = $Object.updated_at diff --git a/src/formats/GitHubEnvironment.Format.ps1xml b/src/formats/GitHubEnvironment.Format.ps1xml index 1373b0c0f..6a8530fa0 100644 --- a/src/formats/GitHubEnvironment.Format.ps1xml +++ b/src/formats/GitHubEnvironment.Format.ps1xml @@ -32,10 +32,24 @@ - Repository + + if ($Host.UI.SupportsVirtualTerminal -and + ($env:GITHUB_ACTIONS -ne 'true')) { + $PSStyle.FormatHyperlink($_.Repository,$_.RepositoryUrl) + } else { + $_.Repository + } + - Owner + + if ($Host.UI.SupportsVirtualTerminal -and + ($env:GITHUB_ACTIONS -ne 'true')) { + $PSStyle.FormatHyperlink($_.Owner,$_.OwnerUrl) + } else { + $_.Owner + } + From 8ef51f0a54bee65aabc939d68b1fc16aa388208f Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 10 Sep 2025 09:59:30 +0200 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=A9=B9=20[Patch]:=20Sort=20the=20retu?= =?UTF-8?q?rn=20for=20`Get-GitHubContext`=20(#505)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This pull request focuses on improving the usability and consistency of the GitHub context management PowerShell module. The main changes include renaming parameter sets and internal logic for clarity, ensuring output is sorted for better user experience, and updating the context completer for more accurate and user-friendly suggestions. - Fixes #503 **Parameter set and logic improvements:** * Renamed parameter sets in `Get-GitHubContext.ps1` for clarity (e.g., `'NamedContext'` to `'Get a named context'`, `'ListAvailableContexts'` to `'List all available contexts'`) and updated corresponding internal logic and debug messages for consistency. **Output and completion enhancements:** * Sorted the output of contexts by name in `Get-GitHubContext.ps1` to provide a more organized and predictable result. * Moved `completers.ps1` from the private to public directory, and updated the context completer to suppress debug output, sort and deduplicate context names, and improve the accuracy of completion suggestions. ## Type of change - [ ] 📖 [Docs] - [ ] 🪲 [Fix] - [x] 🩹 [Patch] - [ ] ⚠️ [Security fix] - [ ] 🚀 [Feature] - [ ] 🌟 [Breaking change] ## Checklist - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas --- .../public/Auth/Context/Get-GitHubContext.ps1 | 15 +++++++-------- .../Auth/Context/completers.ps1 | 3 ++- ...nect-GitHubApp_completer.ps1 => completer.ps1} | 0 3 files changed, 9 insertions(+), 9 deletions(-) rename src/functions/{private => public}/Auth/Context/completers.ps1 (92%) rename src/functions/public/Auth/{Connect-GitHubApp_completer.ps1 => completer.ps1} (100%) diff --git a/src/functions/public/Auth/Context/Get-GitHubContext.ps1 b/src/functions/public/Auth/Context/Get-GitHubContext.ps1 index 9f0015551..6ff8aecc1 100644 --- a/src/functions/public/Auth/Context/Get-GitHubContext.ps1 +++ b/src/functions/public/Auth/Context/Get-GitHubContext.ps1 @@ -19,12 +19,12 @@ Justification = 'Encapsulated in a function. Never leaves as a plain text.' )] [OutputType([GitHubContext])] - [CmdletBinding(DefaultParameterSetName = '__AllParameterSets')] + [CmdletBinding(DefaultParameterSetName = 'Get default context')] param( # The name of the context. [Parameter( Mandatory, - ParameterSetName = 'NamedContext' + ParameterSetName = 'Get a named context' )] [Alias('Name')] [string] $Context, @@ -32,7 +32,7 @@ # List all available contexts. [Parameter( Mandatory, - ParameterSetName = 'ListAvailableContexts' + ParameterSetName = 'List all available contexts' )] [switch] $ListAvailable ) @@ -45,11 +45,11 @@ process { switch ($PSCmdlet.ParameterSetName) { - 'NamedContext' { - Write-Debug "NamedContext: [$Context]" + 'Get a named context' { + Write-Debug "Get a named context: [$Context]" $ID = $Context } - 'ListAvailableContexts' { + 'List all available contexts' { Write-Debug "ListAvailable: [$ListAvailable]" $ID = '*' } @@ -85,7 +85,7 @@ throw "Unknown context type: [$($contextObj.Type)]" } } - } + } | Sort-Object -Property Name } end { @@ -93,4 +93,3 @@ } } #Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.3' } - diff --git a/src/functions/private/Auth/Context/completers.ps1 b/src/functions/public/Auth/Context/completers.ps1 similarity index 92% rename from src/functions/private/Auth/Context/completers.ps1 rename to src/functions/public/Auth/Context/completers.ps1 index 31f4d4436..fb204c67a 100644 --- a/src/functions/private/Auth/Context/completers.ps1 +++ b/src/functions/public/Auth/Context/completers.ps1 @@ -12,7 +12,8 @@ $contexts += 'Anonymous' } - $contexts += (Get-GitHubContext -ListAvailable -Verbose:$false).Name + $contexts += (Get-GitHubContext -ListAvailable -Verbose:$false -Debug:$false).Name + $contexts = $contexts | Sort-Object -Unique $contexts | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } diff --git a/src/functions/public/Auth/Connect-GitHubApp_completer.ps1 b/src/functions/public/Auth/completer.ps1 similarity index 100% rename from src/functions/public/Auth/Connect-GitHubApp_completer.ps1 rename to src/functions/public/Auth/completer.ps1 From 8972167f477598f4cf56fd23ef921483ccba7e2a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Sep 2025 14:54:20 +0200 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=A9=B9=20[Patch]:=20Adjust=20GitHubRe?= =?UTF-8?q?pository=20format=20to=20show=20aligned=20sizes=20(#507)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request updates the file size formatting logic in `GitHubFormatter` to ensure consistent output with two decimal places for all units, including bytes, and adjusts the related tests to match the new output format. The most important changes are grouped below: - Fixes #506 File size formatting improvements: * Modified `FormatFileSize` in `GitHubFormatter.ps1` to always display byte values with two decimal places (e.g., "0.00 B" instead of "0 B"). Test updates for new formatting: * Updated test cases in `GitHubFormatter.Tests.ps1` to expect two-decimal formatting for bytes and adjusted regex patterns to match the new output (e.g., `\d+[.,]\d{2}\s{2}B`). * Refactored the test to directly validate the output of the formatter against the new expected patterns, ensuring the tests accurately reflect the updated formatting logic. * Removed outdated comments and redundant test setup to streamline and clarify the test file. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: MariusStorhaug <17722253+MariusStorhaug@users.noreply.github.com> Co-authored-by: Marius Storhaug --- src/classes/public/GitHubFormatter.ps1 | 2 +- tests/GitHubFormatter.Tests.ps1 | 34 ++++++++++---------------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/classes/public/GitHubFormatter.ps1 b/src/classes/public/GitHubFormatter.ps1 index 1fa06d0c5..b69263dad 100644 --- a/src/classes/public/GitHubFormatter.ps1 +++ b/src/classes/public/GitHubFormatter.ps1 @@ -28,6 +28,6 @@ { $_ -ge 1MB } { return '{0:N2} MB' -f ($size / 1MB) } { $_ -ge 1KB } { return '{0:N2} KB' -f ($size / 1KB) } } - return "$size B" + return '{0:N2} B' -f $size } } diff --git a/tests/GitHubFormatter.Tests.ps1 b/tests/GitHubFormatter.Tests.ps1 index 63f55c7c3..7c9455acf 100644 --- a/tests/GitHubFormatter.Tests.ps1 +++ b/tests/GitHubFormatter.Tests.ps1 @@ -7,11 +7,7 @@ [CmdletBinding()] param() -# This test file validates size property standardization across GitHub classes -# It focuses on unit conversion and formatting expectations rather than live API calls - Describe 'Size Property Standardization Tests' { - Context 'Unit Conversion Logic' { It 'Validates KB to Bytes conversion formula' { # Test the conversion used in GitHubRepository and GitHubOrganization @@ -41,23 +37,19 @@ Describe 'Size Property Standardization Tests' { } Context 'Expected Format Output Patterns' { - It 'Validates expected format patterns for size display' { - # These tests verify the expected output patterns without requiring the actual formatter - # They document what the GitHubFormatter::FormatFileSize method should produce - - $testCases = @( - @{ Bytes = 0; ExpectedPattern = '\d+\s+B' } # "0 B" - @{ Bytes = 512; ExpectedPattern = '\d+\s+B' } # "512 B" - @{ Bytes = 1024; ExpectedPattern = '\d+\.\d{2} KB' } # "1.00 KB" - @{ Bytes = 1048576; ExpectedPattern = '\d+\.\d{2} MB' } # "1.00 MB" - @{ Bytes = 1073741824; ExpectedPattern = '\d+\.\d{2} GB' } # "1.00 GB" - @{ Bytes = 110592; ExpectedPattern = '\d+\.\d{2} KB' } # "108.00 KB" - ) - - foreach ($case in $testCases) { - # Document expected pattern - actual formatting tested in integration tests - $case.ExpectedPattern | Should -Match '\w+' # Verify pattern is non-empty - } + $testCases = @( + @{ Bytes = 0; ExpectedPattern = '\d+[.,]\d{2}\s{2}B' } # "0.00 B" + @{ Bytes = 512; ExpectedPattern = '\d+[.,]\d{2}\s{2}B' } # "512.00 B" + @{ Bytes = 1024; ExpectedPattern = '\d+[.,]\d{2} KB' } # "1.00 KB" + @{ Bytes = 1048576; ExpectedPattern = '\d+[.,]\d{2} MB' } # "1.00 MB" + @{ Bytes = 1073741824; ExpectedPattern = '\d+[.,]\d{2} GB' } # "1.00 GB" + @{ Bytes = 110592; ExpectedPattern = '\d+[.,]\d{2} KB' } # "108.00 KB" + ) + + It 'Validates formatter output pattern for bytes' -ForEach $testCases { + # Test the formatter against the expected pattern + $result = [GitHubFormatter]::FormatFileSize($Bytes) + $result | Should -Match $ExpectedPattern } }