diff --git a/.github/workflows/Process-PSModule.yml b/.github/workflows/Process-PSModule.yml index 9110efba6..0739b4def 100644 --- a/.github/workflows/Process-PSModule.yml +++ b/.github/workflows/Process-PSModule.yml @@ -36,3 +36,5 @@ jobs: secrets: inherit with: SkipTests: SourceCode + Debug: true + Verbose: true diff --git a/src/classes/public/Context/GitHubContext.ps1 b/src/classes/public/Context/GitHubContext.ps1 index 199606c0d..e41175727 100644 --- a/src/classes/public/Context/GitHubContext.ps1 +++ b/src/classes/public/Context/GitHubContext.ps1 @@ -4,6 +4,10 @@ # github.com/Octocat [string] $Name + # The display name of the context. + # Octocat + [string] $DisplayName + # The context type # User / App / Installation [string] $Type diff --git a/src/classes/public/Context/GitHubContext/AppGitHubContext.ps1 b/src/classes/public/Context/GitHubContext/AppGitHubContext.ps1 index 1abd2444b..3bd58725f 100644 --- a/src/classes/public/Context/GitHubContext/AppGitHubContext.ps1 +++ b/src/classes/public/Context/GitHubContext/AppGitHubContext.ps1 @@ -2,6 +2,18 @@ # Client ID for GitHub Apps [string] $ClientID + # Owner of the GitHub App + [string] $OwnerName + + # Type of the owner of the GitHub App + [string] $OwnerType + + # The permissions that the app is requesting on the target + [string[]] $Permissions + + # The events that the app is subscribing to once installed + [string[]] $Events + # Creates a context object from a hashtable of key-vaule pairs. AppGitHubContext([hashtable]$Properties) { foreach ($Property in $Properties.Keys) { diff --git a/src/classes/public/Context/GitHubContext/InstallationGitHubContext.ps1 b/src/classes/public/Context/GitHubContext/InstallationGitHubContext.ps1 index bd4f35739..d4a97368c 100644 --- a/src/classes/public/Context/GitHubContext/InstallationGitHubContext.ps1 +++ b/src/classes/public/Context/GitHubContext/InstallationGitHubContext.ps1 @@ -9,6 +9,18 @@ # The installation ID. [int] $InstallationID + # The permissions that the app is requesting on the target + [string[]] $Permissions + + # The events that the app is subscribing to once installed + [string[]] $Events + + # The target type of the installation. + [string] $TargetType + + # The target login of the installation. + [string] $TargetName + # Creates a context object from a hashtable of key-vaule pairs. InstallationGitHubContext([hashtable]$Properties) { foreach ($Property in $Properties.Keys) { diff --git a/src/functions/public/API/Invoke-GitHubAPI.ps1 b/src/functions/public/API/Invoke-GitHubAPI.ps1 index c8733ad93..7f9dc0dc2 100644 --- a/src/functions/public/API/Invoke-GitHubAPI.ps1 +++ b/src/functions/public/API/Invoke-GitHubAPI.ps1 @@ -182,12 +182,8 @@ Write-Debug 'Response:' $response | Out-String -Stream | ForEach-Object { Write-Debug $_ } Write-Debug '---------------------------' - Write-Debug $headers.'Content-Type' switch -Regex ($headers.'Content-Type') { - 'application/json' { - $results = $response.Content | ConvertFrom-Json - } - 'application/vnd.github.v3+json' { + 'application/.*json' { $results = $response.Content | ConvertFrom-Json } 'text/plain' { @@ -198,6 +194,10 @@ $results = [System.Text.Encoding]::UTF8.GetString($byteArray) } default { + if (-not $response.Content) { + $results = $null + break + } Write-Warning "Unknown content type: $($headers.'Content-Type')" Write-Warning 'Please report this issue!' [byte[]]$byteArray = $response.Content diff --git a/src/functions/public/Auth/Connect-GitHubAccount.ps1 b/src/functions/public/Auth/Connect-GitHubAccount.ps1 index 14c8ad999..c17e7ad63 100644 --- a/src/functions/public/Auth/Connect-GitHubAccount.ps1 +++ b/src/functions/public/Auth/Connect-GitHubAccount.ps1 @@ -103,6 +103,12 @@ )] [string] $PrivateKey, + # Skip loading GitHub App contexts. + [Parameter( + ParameterSetName = 'App' + )] + [switch] $SkipAppAutoload, + # The default enterprise to use in commands. [Parameter()] [string] $Enterprise, @@ -141,165 +147,181 @@ [switch] $NotDefault ) - $commandName = $MyInvocation.MyCommand.Name - Write-Verbose "[$commandName] - Start" + begin { + $commandName = $MyInvocation.MyCommand.Name + Write-Verbose "[$commandName] - Start" + } - try { - if ($Token -is [System.Security.SecureString]) { - $Token = ConvertFrom-SecureString $Token -AsPlainText - } + process { + try { + if ($Token -is [System.Security.SecureString]) { + $Token = ConvertFrom-SecureString $Token -AsPlainText + } - if (-not $HostName) { - $HostName = $script:GitHub.Config.HostName - } - $HostName = $HostName -replace '^https?://' - $ApiBaseUri = "https://api.$HostName" - $authType = $PSCmdlet.ParameterSetName - - # If running on GitHub Actions and no access token is provided, use the GitHub token. - if (($env:GITHUB_ACTIONS -eq 'true') -and $PSCmdlet.ParameterSetName -ne 'App') { - $customTokenProvided = -not [string]::IsNullOrEmpty($Token) - $gitHubToken = $env:GH_TOKEN ?? $env:GITHUB_TOKEN - $gitHubTokenPresent = -not [string]::IsNullOrEmpty($gitHubToken) - Write-Verbose "A token was provided: [$customTokenProvided]" - Write-Verbose "Detected GitHub token: [$gitHubTokenPresent]" - if (-not $customTokenProvided -and $gitHubTokenPresent) { - $authType = 'Token' - $Token = $gitHubToken + if (-not $HostName) { + $HostName = $script:GitHub.Config.HostName + } + $HostName = $HostName -replace '^https?://' + $ApiBaseUri = "https://api.$HostName" + $authType = $PSCmdlet.ParameterSetName + + # If running on GitHub Actions and no access token is provided, use the GitHub token. + if (($env:GITHUB_ACTIONS -eq 'true') -and $PSCmdlet.ParameterSetName -ne 'App') { + $customTokenProvided = -not [string]::IsNullOrEmpty($Token) + $gitHubToken = $env:GH_TOKEN ?? $env:GITHUB_TOKEN + $gitHubTokenPresent = -not [string]::IsNullOrEmpty($gitHubToken) + Write-Verbose "A token was provided: [$customTokenProvided]" + Write-Verbose "Detected GitHub token: [$gitHubTokenPresent]" + if (-not $customTokenProvided -and $gitHubTokenPresent) { + $authType = 'Token' + $Token = $gitHubToken + } } - } - $context = @{ - ApiBaseUri = $ApiBaseUri - ApiVersion = $ApiVersion - HostName = $HostName - AuthType = $authType - Enterprise = $Enterprise - Owner = $Owner - Repo = $Repo - } + $context = @{ + ApiBaseUri = $ApiBaseUri + ApiVersion = $ApiVersion + HostName = $HostName + AuthType = $authType + Enterprise = $Enterprise + Owner = $Owner + Repo = $Repo + } - Write-Verbose ($context | Format-Table | Out-String) + Write-Verbose ($context | Format-Table | Out-String) + + switch ($authType) { + 'UAT' { + Write-Verbose 'Logging in using device flow...' + if (-not [string]::IsNullOrEmpty($ClientID)) { + Write-Verbose "Using provided ClientID: [$ClientID]" + $authClientID = $ClientID + } else { + switch ($Mode) { + 'GitHubApp' { + Write-Verbose "Using default ClientID: [$($script:GitHub.Config.GitHubAppClientID)']" + $authClientID = $($script:GitHub.Config.GitHubAppClientID) + } + 'OAuthApp' { + Write-Verbose "Using default ClientID: [$($script:GitHub.Config.OAuthAppClientID)]" + $authClientID = $($script:GitHub.Config.OAuthAppClientID) + } + default { + Write-Warning '⚠ ' -ForegroundColor Yellow -NoNewline + Write-Warning "Unexpected authentication mode: $Mode" + return + } + } + } + Write-Verbose "Using $Mode authentication..." + $tokenResponse = Invoke-GitHubDeviceFlowLogin -ClientID $authClientID -Scope $Scope -HostName $HostName - switch ($authType) { - 'UAT' { - Write-Verbose 'Logging in using device flow...' - if (-not [string]::IsNullOrEmpty($ClientID)) { - Write-Verbose "Using provided ClientID: [$ClientID]" - $authClientID = $ClientID - } else { switch ($Mode) { 'GitHubApp' { - Write-Verbose "Using default ClientID: [$($script:GitHub.Config.GitHubAppClientID)']" - $authClientID = $($script:GitHub.Config.GitHubAppClientID) + $context += @{ + Token = ConvertTo-SecureString -AsPlainText $tokenResponse.access_token + TokenExpirationDate = (Get-Date).AddSeconds($tokenResponse.expires_in) + TokenType = $tokenResponse.access_token -replace $script:GitHub.TokenPrefixPattern + AuthClientID = $authClientID + DeviceFlowType = $Mode + RefreshToken = ConvertTo-SecureString -AsPlainText $tokenResponse.refresh_token + RefreshTokenExpirationDate = (Get-Date).AddSeconds($tokenResponse.refresh_token_expires_in) + Scope = $tokenResponse.scope + } } 'OAuthApp' { - Write-Verbose "Using default ClientID: [$($script:GitHub.Config.OAuthAppClientID)]" - $authClientID = $($script:GitHub.Config.OAuthAppClientID) + $context += @{ + Token = ConvertTo-SecureString -AsPlainText $tokenResponse.access_token + TokenType = $tokenResponse.access_token -replace $script:GitHub.TokenPrefixPattern + AuthClientID = $authClientID + DeviceFlowType = $Mode + Scope = $tokenResponse.scope + } } default { - Write-Warning '⚠ ' -ForegroundColor Yellow -NoNewline - Write-Warning "Unexpected authentication mode: $Mode" + Write-Host '⚠ ' -ForegroundColor Yellow -NoNewline + Write-Host "Unexpected authentication mode: $Mode" return } } } - Write-Verbose "Using $Mode authentication..." - $tokenResponse = Invoke-GitHubDeviceFlowLogin -ClientID $authClientID -Scope $Scope -HostName $HostName - - switch ($Mode) { - 'GitHubApp' { - $context += @{ - Token = ConvertTo-SecureString -AsPlainText $tokenResponse.access_token - TokenExpirationDate = (Get-Date).AddSeconds($tokenResponse.expires_in) - TokenType = $tokenResponse.access_token -replace $script:GitHub.TokenPrefixPattern - AuthClientID = $authClientID - DeviceFlowType = $Mode - RefreshToken = ConvertTo-SecureString -AsPlainText $tokenResponse.refresh_token - RefreshTokenExpirationDate = (Get-Date).AddSeconds($tokenResponse.refresh_token_expires_in) - Scope = $tokenResponse.scope - } - } - 'OAuthApp' { - $context += @{ - Token = ConvertTo-SecureString -AsPlainText $tokenResponse.access_token - TokenType = $tokenResponse.access_token -replace $script:GitHub.TokenPrefixPattern - AuthClientID = $authClientID - DeviceFlowType = $Mode - Scope = $tokenResponse.scope - } - } - default { - Write-Host '⚠ ' -ForegroundColor Yellow -NoNewline - Write-Host "Unexpected authentication mode: $Mode" - return + 'App' { + Write-Verbose 'Logging in as a GitHub App...' + $context += @{ + Token = ConvertTo-SecureString -AsPlainText $PrivateKey + TokenType = 'PEM' + ClientID = $ClientID } } - } - 'App' { - Write-Verbose 'Logging in as a GitHub App...' - $context += @{ - Token = ConvertTo-SecureString -AsPlainText $PrivateKey - TokenType = 'PEM' - ClientID = $ClientID - } - } - 'PAT' { - Write-Debug "UseAccessToken is set to [$UseAccessToken]. Using provided access token..." - Write-Verbose 'Logging in using personal access token...' - Write-Host '! ' -ForegroundColor DarkYellow -NoNewline - Start-Process "https://$HostName/settings/tokens" - $accessTokenValue = Read-Host -Prompt 'Enter your personal access token' -AsSecureString - $Token = ConvertFrom-SecureString $accessTokenValue -AsPlainText - $tokenType = $Token -replace $script:GitHub.TokenPrefixPattern - $context += @{ - Token = ConvertTo-SecureString -AsPlainText $Token - TokenType = $tokenType + 'PAT' { + Write-Debug "UseAccessToken is set to [$UseAccessToken]. Using provided access token..." + Write-Verbose 'Logging in using personal access token...' + Write-Host '! ' -ForegroundColor DarkYellow -NoNewline + Start-Process "https://$HostName/settings/tokens" + $accessTokenValue = Read-Host -Prompt 'Enter your personal access token' -AsSecureString + $Token = ConvertFrom-SecureString $accessTokenValue -AsPlainText + $tokenType = $Token -replace $script:GitHub.TokenPrefixPattern + $context += @{ + Token = ConvertTo-SecureString -AsPlainText $Token + TokenType = $tokenType + } } - } - 'Token' { - $tokenType = $Token -replace $script:GitHub.TokenPrefixPattern - switch -Regex ($tokenType) { - 'ghp|github_pat' { - $context += @{ - Token = ConvertTo-SecureString -AsPlainText $Token - TokenType = $tokenType + 'Token' { + $tokenType = $Token -replace $script:GitHub.TokenPrefixPattern + switch -Regex ($tokenType) { + 'ghp|github_pat' { + Write-Verbose 'Logging in using a user access token...' + $context += @{ + Token = ConvertTo-SecureString -AsPlainText $Token + TokenType = $tokenType + } + $context['AuthType'] = 'PAT' } - $context['AuthType'] = 'PAT' - } - 'ghs' { - Write-Verbose 'Logging in using an installation access token...' - $context += @{ - Token = ConvertTo-SecureString -AsPlainText $Token - TokenType = $tokenType + 'ghs' { + Write-Verbose 'Logging in using an installation access token...' + $context += @{ + Token = ConvertTo-SecureString -AsPlainText $Token + TokenType = $tokenType + } + $context['AuthType'] = 'IAT' + } + default { + Write-Host '⚠ ' -ForegroundColor Yellow -NoNewline + Write-Host "Unexpected token type: $tokenType" + throw "Unexpected token type: $tokenType" } - $context['AuthType'] = 'IAT' - } - default { - Write-Host '⚠ ' -ForegroundColor Yellow -NoNewline - Write-Host "Unexpected token type: $tokenType" - throw "Unexpected token type: $tokenType" } } } + $contextObj = Set-GitHubContext -Context $context -Default:(!$NotDefault) -PassThru + Write-Verbose ($contextObj | Format-List | Out-String) + if (-not $Silent) { + $name = $contextObj.Username + Write-Host '✓ ' -ForegroundColor Green -NoNewline + Write-Host "Logged in as $name!" + } + + if ($authType -eq 'App' -and -not $SkipAppAutoload) { + Write-Verbose 'Loading GitHub App contexts...' + Get-GitHubApp + } + + } catch { + Write-Error $_ + Write-Error (Get-PSCallStack | Format-Table | Out-String) + throw 'Failed to connect to GitHub.' } - $contextObj = Set-GitHubContext -Context $context -Default:(!$NotDefault) -PassThru - Write-Verbose ($contextObj | Format-List | Out-String) - if (-not $Silent) { - $name = $contextObj.Username - Write-Host '✓ ' -ForegroundColor Green -NoNewline - Write-Host "Logged in as $name!" - } - } catch { - Write-Error $_ - Write-Error (Get-PSCallStack | Format-Table | Out-String) - throw 'Failed to connect to GitHub.' - } finally { + } + + end { + Write-Verbose "[$commandName] - End" + } + + clean { Remove-Variable -Name tokenResponse -ErrorAction SilentlyContinue Remove-Variable -Name context -ErrorAction SilentlyContinue Remove-Variable -Name contextData -ErrorAction SilentlyContinue Remove-Variable -Name Token -ErrorAction SilentlyContinue [System.GC]::Collect() } - Write-Verbose "[$commandName] - End" } diff --git a/src/functions/public/Auth/Connect-GitHubApp.ps1 b/src/functions/public/Auth/Connect-GitHubApp.ps1 index 492efec9f..d4822f391 100644 --- a/src/functions/public/Auth/Connect-GitHubApp.ps1 +++ b/src/functions/public/Auth/Connect-GitHubApp.ps1 @@ -109,16 +109,25 @@ InstallationID = $installation.id Token = $token.Token TokenExpirationDate = $token.ExpiresAt + Permissions = $installation.permissions + Events = $installation.events + TargetType = $installation.target_type } switch ($installation.target_type) { 'User' { - $contextParams['Owner'] = $installation.account.login + $contextParams += @{ + TargetName = $installation.account.login + } } 'Organization' { - $contextParams['Owner'] = $installation.account.login + $contextParams += @{ + TargetName = $installation.account.login + } } 'Enterprise' { - $contextParams['Enterprise'] = $installation.account.slug + $contextParams += @{ + TargetName = $installation.account.slug + } } } Write-Verbose 'Logging in using an installation access token...' diff --git a/src/functions/public/Auth/Context/Set-GitHubContext.ps1 b/src/functions/public/Auth/Context/Set-GitHubContext.ps1 index 2555d106c..e5ee488fb 100644 --- a/src/functions/public/Auth/Context/Set-GitHubContext.ps1 +++ b/src/functions/public/Auth/Context/Set-GitHubContext.ps1 @@ -54,11 +54,13 @@ function Set-GitHubContext { switch -Regex ($($Context['AuthType'])) { 'PAT|UAT|IAT' { $viewer = Get-GitHubViewer -Context $Context - $login = $viewer.login + $viewer | Out-String -Stream | ForEach-Object { Write-Verbose $_ } + $login = [string]$viewer.login $Context += @{ - Username = $login - NodeID = $viewer.id - DatabaseID = ($viewer.databaseId).ToString() + DisplayName = [string]$viewer.name + Username = $login + NodeID = [string]$viewer.id + DatabaseID = [string]$viewer.databaseId } } 'PAT|UAT' { @@ -79,11 +81,16 @@ function Set-GitHubContext { $app = Get-GitHubApp -Context $Context $ContextName = "$($Context['HostName'])/$($app.slug)" $Context += @{ - Name = $ContextName - Username = $app.slug - NodeID = $app.node_id - DatabaseID = $app.id - Type = 'App' + Name = $ContextName + DisplayName = [string]$app.name + Username = [string]$app.slug + NodeID = [string]$app.node_id + DatabaseID = [string]$app.id + Permissions = [string]$app.permissions + Events = [string]$app.events + OwnerName = [string]$app.owner.login + OwnerType = [string]$app.owner.type + Type = 'App' } } default { @@ -92,8 +99,9 @@ function Set-GitHubContext { } Write-Verbose "Found [$($Context['Type'])] with login: [$($Context['Name'])]" $Context | Out-String -Stream | ForEach-Object { Write-Verbose $_ } - + Write-Verbose '----------------------------------------------------' if ($PSCmdlet.ShouldProcess('Context', 'Set')) { + Write-Verbose "Saving context: [$($script:GitHub.Config.ID)/$($Context['Name'])]" Set-Context -ID "$($script:GitHub.Config.ID)/$($Context['Name'])" -Context $Context if ($Default) { Set-GitHubDefaultContext -Context $Context['Name'] @@ -106,7 +114,8 @@ function Set-GitHubContext { } } } catch { - throw ($_ -join ';') + Write-Error $_ | Select-Object * + throw 'Failed to set the GitHub context.' } } diff --git a/src/functions/public/Auth/Get-GitHubViewer.ps1 b/src/functions/public/Auth/Get-GitHubViewer.ps1 index 36e6e05ec..7faf96027 100644 --- a/src/functions/public/Auth/Get-GitHubViewer.ps1 +++ b/src/functions/public/Auth/Get-GitHubViewer.ps1 @@ -18,7 +18,7 @@ param( # The fields to return. [Parameter()] - [string[]] $Fields = @('login', 'id', 'databaseId'), + [string[]] $Fields = @('name', 'login', 'id', 'databaseId'), # The context to run the command in. Used to get the details for the API call. # Can be either a string or a GitHubContext object. diff --git a/src/functions/public/GraphQL/Invoke-GitHubGraphQLQuery.ps1 b/src/functions/public/GraphQL/Invoke-GitHubGraphQLQuery.ps1 index 6f5ae2c52..e6050a832 100644 --- a/src/functions/public/GraphQL/Invoke-GitHubGraphQLQuery.ps1 +++ b/src/functions/public/GraphQL/Invoke-GitHubGraphQLQuery.ps1 @@ -25,19 +25,29 @@ [object] $Context = (Get-GitHubContext) ) - $Context = Resolve-GitHubContext -Context $Context - - $inputObject = @{ - Context = $Context - APIEndpoint = '/graphql' - Method = 'Post' - Body = @{ - 'query' = $query - 'variables' = $variables - } | ConvertTo-Json + begin { + $commandName = $MyInvocation.MyCommand.Name + Write-Verbose "[$commandName] - Start" + $Context = Resolve-GitHubContext -Context $Context } - Invoke-GitHubAPI @inputObject | ForEach-Object { - Write-Output $_.Response + process { + $inputObject = @{ + Context = $Context + APIEndpoint = '/graphql' + Method = 'Post' + Body = @{ + 'query' = $Query + 'variables' = $Variables + } | ConvertTo-Json + } + + Invoke-GitHubAPI @inputObject | ForEach-Object { + Write-Output $_.Response + } + } + + end { + Write-Verbose "[$commandName] - End" } }