diff --git a/src/functions/private/Commands/Initialize-RunnerEnvironment.ps1 b/src/functions/private/Commands/Initialize-RunnerEnvironment.ps1 index d8c13cf71..254f8b59d 100644 --- a/src/functions/private/Commands/Initialize-RunnerEnvironment.ps1 +++ b/src/functions/private/Commands/Initialize-RunnerEnvironment.ps1 @@ -18,11 +18,10 @@ $env:GITHUB_REPOSITORY_NAME = $env:GITHUB_REPOSITORY -replace '.+/' Set-GitHubEnv -Name 'GITHUB_REPOSITORY_NAME' -Value $env:GITHUB_REPOSITORY_NAME - # Autologon if a token is present in environment variables - Write-Verbose (Get-ChildItem -Path 'Env:' | Where-Object Name -In 'GH_TOKEN', 'GITHUB_TOKEN' | Out-String) - $tokenVar = Get-ChildItem -Path 'Env:' | Where-Object Name -In 'GH_TOKEN', 'GITHUB_TOKEN' | Select-Object -First 1 -ExpandProperty Value - $tokenVarPresent = $tokenVar.count -gt 0 -and -not [string]::IsNullOrEmpty($tokenVar) - if ($tokenVarPresent) { - Connect-GitHubAccount -Repo $env:GITHUB_REPOSITORY_NAME -Owner $env:GITHUB_REPOSITORY_OWNER -Server $env:GITHUB_SERVER_URL + $params = @{ + Owner = $env:GITHUB_REPOSITORY_OWNER + Repo = $env:GITHUB_REPOSITORY_NAME + Server = $env:GITHUB_SERVER_URL } + Connect-GitHubAccount @params } diff --git a/src/functions/public/Auth/Connect-GitHubAccount.ps1 b/src/functions/public/Auth/Connect-GitHubAccount.ps1 index 3cbe84c88..a81c9f133 100644 --- a/src/functions/public/Auth/Connect-GitHubAccount.ps1 +++ b/src/functions/public/Auth/Connect-GitHubAccount.ps1 @@ -52,7 +52,6 @@ [Alias('Login-GH')] [OutputType([void])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidLongLines', '', Justification = 'Long links for documentation.')] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AccessToken', Justification = 'Required for parameter set')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Is the CLI part of the module.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'The tokens are recieved as clear text. Mitigating exposure by removing variables and performing garbage collection.')] [CmdletBinding(DefaultParameterSetName = 'UAT')] @@ -71,14 +70,19 @@ [Parameter(ParameterSetName = 'UAT')] [string] $Scope = 'gist read:org repo workflow', - # The personal access token to use for authentication. + # An access token to use for authentication. If left enpty, the user will be prompted to enter the token. + # Supports both personal access tokens and GitHub App installation access tokens. + # Example: 'ghp_1234567890abcdef' + # Example: 'ghs_1234567890abcdef' [Parameter( Mandatory, - ParameterSetName = 'PAT' + ParameterSetName = 'Token' )] + [AllowNull()] [Alias('Token')] [Alias('PAT')] - [switch] $AccessToken, + [Alias('IAT')] + [string] $AccessToken, # The client ID for the GitHub App to use for authentication. [Parameter(ParameterSetName = 'UAT')] @@ -99,12 +103,12 @@ [Parameter()] [Alias('Organization')] [Alias('Org')] - [string] $Owner, + [string] $Owner = $env:GITHUB_REPOSITORY_OWNER, # Set the default repository to use in commands. [Parameter()] [Alias('Repository')] - [string] $Repo, + [string] $Repo = $env:GITHUB_REPOSITORY_NAME, # API version used for API requests. [Parameter()] @@ -115,7 +119,7 @@ [Parameter()] [Alias('Host')] [Alias('Server')] - [string] $HostName = 'github.com', + [string] $HostName = $env:GITHUB_SERVER_URL ?? 'github.com', # Suppresses the output of the function. [Parameter()] @@ -124,180 +128,184 @@ [Alias('s')] [switch] $Silent ) + try { + $HostName = $HostName -replace '^https?://' + $ApiBaseUri = "https://api.$HostName" - $HostName = $HostName -replace '^https?://' - $ApiBaseUri = "https://api.$HostName" + # First assume interactive logon + $AuthType = $PSCmdlet.ParameterSetName - $envVars = Get-ChildItem -Path 'Env:' - Write-Debug 'Environment variables:' - Write-Debug ($envVars | Format-Table -AutoSize | Out-String) - $gitHubToken = $envVars | Where-Object Name -In 'GH_TOKEN', 'GITHUB_TOKEN' | Select-Object -First 1 -ExpandProperty Value - Write-Debug "GitHub token: [$gitHubToken]" - $gitHubTokenPresent = $gitHubToken.count -gt 0 -and -not [string]::IsNullOrEmpty($gitHubToken) - Write-Debug "GitHub token present: [$gitHubTokenPresent]" - $AuthType = if ($gitHubTokenPresent) { 'IAT' } else { $PSCmdlet.ParameterSetName } - Write-Verbose "AuthType: [$AuthType]" - switch ($AuthType) { - 'UAT' { - Write-Verbose 'Logging in using device flow...' - $authClientID = $ClientID ?? (Get-GitHubConfig -Name 'AuthClientID') ?? $script:Auth.$Mode.ClientID - if ($Mode -ne (Get-GitHubConfig -Name 'DeviceFlowType' -ErrorAction SilentlyContinue)) { - Write-Verbose "Using $Mode authentication..." - $tokenResponse = Invoke-GitHubDeviceFlowLogin -ClientID $authClientID -Scope $Scope -HostName $HostName - } else { - $accessTokenValidity = [datetime](Get-GitHubConfig -Name 'AccessTokenExpirationDate') - (Get-Date) - $accessTokenIsValid = $accessTokenValidity.Seconds -gt 0 - $hours = $accessTokenValidity.Hours.ToString().PadLeft(2, '0') - $minutes = $accessTokenValidity.Minutes.ToString().PadLeft(2, '0') - $seconds = $accessTokenValidity.Seconds.ToString().PadLeft(2, '0') - $accessTokenValidityText = "$hours`:$minutes`:$seconds" - if ($accessTokenIsValid) { - if ($accessTokenValidity.TotalHours -gt $script:Auth.AccessTokenGracePeriodInHours) { - if (-not $Silent) { - Write-Host '✓ ' -ForegroundColor Green -NoNewline - Write-Host "Access token is still valid for $accessTokenValidityText ..." + Write-Verbose "Running on GitHub Actions: [$env:GITHUB_ACTIONS]" + + # Autologon if running on GitHub Actions and no access token is provided + if ($env:GITHUB_ACTIONS -eq 'true' -and [string]::IsNullOrEmpty($AccessToken)) { + $gitHubToken = $env:GH_TOKEN ?? $env:GITHUB_TOKEN + $gitHubTokenPresent = $gitHubToken.count -gt 0 -and -not [string]::IsNullOrEmpty($gitHubToken) + Write-Debug "GitHub token present: [$gitHubTokenPresent]" + if ($gitHubTokenPresent) { + $AuthType = 'Token' + $AccessToken = $gitHubToken + } + } + + Write-Verbose "AuthType: [$AuthType]" + switch ($AuthType) { + 'UAT' { + Write-Verbose 'Logging in using device flow...' + $authClientID = $ClientID ?? (Get-GitHubConfig -Name 'AuthClientID') ?? $script:Auth.$Mode.ClientID + if ($Mode -ne (Get-GitHubConfig -Name 'DeviceFlowType' -ErrorAction SilentlyContinue)) { + Write-Verbose "Using $Mode authentication..." + $tokenResponse = Invoke-GitHubDeviceFlowLogin -ClientID $authClientID -Scope $Scope -HostName $HostName + } else { + $accessTokenValidity = [datetime](Get-GitHubConfig -Name 'AccessTokenExpirationDate') - (Get-Date) + $accessTokenIsValid = $accessTokenValidity.Seconds -gt 0 + $hours = $accessTokenValidity.Hours.ToString().PadLeft(2, '0') + $minutes = $accessTokenValidity.Minutes.ToString().PadLeft(2, '0') + $seconds = $accessTokenValidity.Seconds.ToString().PadLeft(2, '0') + $accessTokenValidityText = "$hours`:$minutes`:$seconds" + if ($accessTokenIsValid) { + if ($accessTokenValidity.TotalHours -gt $script:Auth.AccessTokenGracePeriodInHours) { + if (-not $Silent) { + Write-Host '✓ ' -ForegroundColor Green -NoNewline + Write-Host "Access token is still valid for $accessTokenValidityText ..." + } + break + } else { + if (-not $Silent) { + Write-Host '⚠ ' -ForegroundColor Yellow -NoNewline + Write-Host "Access token remaining validity $accessTokenValidityText. Refreshing access token..." + } + $tokenResponse = Invoke-GitHubDeviceFlowLogin -ClientID $authClientID -RefreshToken (Get-GitHubConfig -Name 'RefreshToken') -HostName $HostName } - break } else { - if (-not $Silent) { - Write-Host '⚠ ' -ForegroundColor Yellow -NoNewline - Write-Host "Access token remaining validity $accessTokenValidityText. Refreshing access token..." + $refreshTokenValidity = [datetime](Get-GitHubConfig -Name 'RefreshTokenExpirationDate') - (Get-Date) + $refreshTokenIsValid = $refreshTokenValidity.Seconds -gt 0 + if ($refreshTokenIsValid) { + if (-not $Silent) { + Write-Host '⚠ ' -ForegroundColor Yellow -NoNewline + Write-Host 'Access token expired. Refreshing access token...' + } + $tokenResponse = Invoke-GitHubDeviceFlowLogin -ClientID $authClientID -RefreshToken (Get-GitHubConfig -Name 'RefreshToken') -HostName $HostName + } else { + Write-Verbose "Using $Mode authentication..." + $tokenResponse = Invoke-GitHubDeviceFlowLogin -ClientID $authClientID -Scope $Scope -HostName $HostName } - $tokenResponse = Invoke-GitHubDeviceFlowLogin -ClientID $authClientID -RefreshToken (Get-GitHubConfig -Name 'RefreshToken') -HostName $HostName } - } else { - $refreshTokenValidity = [datetime](Get-GitHubConfig -Name 'RefreshTokenExpirationDate') - (Get-Date) - $refreshTokenIsValid = $refreshTokenValidity.Seconds -gt 0 - if ($refreshTokenIsValid) { - if (-not $Silent) { - Write-Host '⚠ ' -ForegroundColor Yellow -NoNewline - Write-Host 'Access token expired. Refreshing access token...' + } + Reset-GitHubConfig -Scope 'Auth' + switch ($Mode) { + 'GitHubApp' { + $settings = @{ + AccessToken = ConvertTo-SecureString -AsPlainText $tokenResponse.access_token + AccessTokenExpirationDate = (Get-Date).AddSeconds($tokenResponse.expires_in) + AccessTokenType = $tokenResponse.access_token -replace '_.*$', '_*' + ApiBaseUri = $ApiBaseUri + ApiVersion = $ApiVersion + AuthClientID = $authClientID + AuthType = $AuthType + DeviceFlowType = $Mode + HostName = $HostName + RefreshToken = ConvertTo-SecureString -AsPlainText $tokenResponse.refresh_token + RefreshTokenExpirationDate = (Get-Date).AddSeconds($tokenResponse.refresh_token_expires_in) + Scope = $tokenResponse.scope + } + } + 'OAuthApp' { + $settings = @{ + AccessToken = ConvertTo-SecureString -AsPlainText $tokenResponse.access_token + AccessTokenType = $tokenResponse.access_token -replace '_.*$', '_*' + ApiBaseUri = $ApiBaseUri + ApiVersion = $ApiVersion + AuthClientID = $authClientID + AuthType = $AuthType + DeviceFlowType = $Mode + HostName = $HostName + Scope = $tokenResponse.scope } - $tokenResponse = Invoke-GitHubDeviceFlowLogin -ClientID $authClientID -RefreshToken (Get-GitHubConfig -Name 'RefreshToken') -HostName $HostName - } else { - Write-Verbose "Using $Mode authentication..." - $tokenResponse = Invoke-GitHubDeviceFlowLogin -ClientID $authClientID -Scope $Scope -HostName $HostName } } + Set-GitHubConfig @settings + $user = Get-GitHubUser + $username = $user.login } - Reset-GitHubConfig -Scope 'Auth' - switch ($Mode) { - 'GitHubApp' { - $settings = @{ - AccessToken = ConvertTo-SecureString -AsPlainText $tokenResponse.access_token - AccessTokenExpirationDate = (Get-Date).AddSeconds($tokenResponse.expires_in) - AccessTokenType = $tokenResponse.access_token -replace '_.*$', '_*' - ApiBaseUri = $ApiBaseUri - ApiVersion = $ApiVersion - AuthClientID = $authClientID - AuthType = $AuthType - DeviceFlowType = $Mode - HostName = $HostName - RefreshToken = ConvertTo-SecureString -AsPlainText $tokenResponse.refresh_token - RefreshTokenExpirationDate = (Get-Date).AddSeconds($tokenResponse.refresh_token_expires_in) - Scope = $tokenResponse.scope - } + 'App' { + Write-Verbose 'Logging in as a GitHub App...' + Reset-GitHubConfig -Scope 'Auth' + $jwt = Get-GitHubAppJWT -ClientId $ClientID -PrivateKey $PrivateKey + $settings = @{ + AccessToken = ConvertTo-SecureString -AsPlainText $jwt + AccessTokenType = 'JWT' + ApiBaseUri = $ApiBaseUri + ApiVersion = $ApiVersion + AuthType = $AuthType + ClientID = $ClientID + HostName = $HostName + } + Set-GitHubConfig @settings + $app = Get-GitHubApp + $username = $app.slug + } + 'Token' { + if ([string]::IsNullOrEmpty($AccessToken)) { + 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 + $AccessToken = ConvertFrom-SecureString $accessTokenValue -AsPlainText } - 'OAuthApp' { - $settings = @{ - AccessToken = ConvertTo-SecureString -AsPlainText $tokenResponse.access_token - AccessTokenType = $tokenResponse.access_token -replace '_.*$', '_*' - ApiBaseUri = $ApiBaseUri - ApiVersion = $ApiVersion - AuthClientID = $authClientID - AuthType = $AuthType - DeviceFlowType = $Mode - HostName = $HostName - Scope = $tokenResponse.scope + $accessTokenType = $AccessToken -replace '_.*$', '_*' + switch -Regex ($accessTokenType) { + '^ghp_|^github_pat_' { + Reset-GitHubConfig -Scope 'Auth' + $settings = @{ + AccessToken = $accessTokenValue + AccessTokenType = $accessTokenType + ApiBaseUri = $ApiBaseUri + ApiVersion = $ApiVersion + AuthType = 'PAT' + HostName = $HostName + } + Set-GitHubConfig @settings + } + '^ghs_' { + Write-Verbose 'Logging in using GitHub access token...' + Reset-GitHubConfig -Scope 'Auth' + $settings = @{ + AccessToken = ConvertTo-SecureString -AsPlainText $AccessToken + AccessTokenType = $accessTokenType + ApiBaseUri = $ApiBaseUri + ApiVersion = $ApiVersion + AuthType = 'IAT' + HostName = $HostName + } + Set-GitHubConfig @settings + $username = 'system' + } + default { + Write-Host '⚠ ' -ForegroundColor Yellow -NoNewline + Write-Host "Unexpected access token format: $accessTokenType" } } } - Set-GitHubConfig @settings - break } - 'PAT' { - Write-Verbose 'Logging in using personal access token...' - Reset-GitHubConfig -Scope 'Auth' - Write-Host '! ' -ForegroundColor DarkYellow -NoNewline - Start-Process "https://$HostName/settings/tokens" - $accessTokenValue = Read-Host -Prompt 'Enter your personal access token' -AsSecureString - $accessTokenType = (ConvertFrom-SecureString $accessTokenValue -AsPlainText) -replace '_.*$', '_*' - if ($accessTokenType -notmatch '^ghp_|^github_pat_') { - Write-Host '⚠ ' -ForegroundColor Yellow -NoNewline - Write-Host "Unexpected access token format: $accessTokenType" - } - $settings = @{ - AccessToken = $accessTokenValue - AccessTokenType = $accessTokenType - ApiBaseUri = $ApiBaseUri - ApiVersion = $ApiVersion - AuthType = $AuthType - HostName = $HostName - } - Set-GitHubConfig @settings - break - } - 'App' { - Write-Verbose 'Logging in as a GitHub App...' - Reset-GitHubConfig -Scope 'Auth' - $jwt = Get-GitHubAppJWT -ClientId $ClientID -PrivateKey $PrivateKey - $settings = @{ - AccessToken = ConvertTo-SecureString -AsPlainText $jwt - AccessTokenType = 'JWT' - ApiBaseUri = $ApiBaseUri - ApiVersion = $ApiVersion - AuthType = $AuthType - ClientID = $ClientID - HostName = $HostName - } - Set-GitHubConfig @settings - } - 'IAT' { - Write-Verbose 'Logging in using GitHub access token...' - Reset-GitHubConfig -Scope 'Auth' - $prefix = $gitHubToken -replace '_.*$', '_*' - $settings = @{ - AccessToken = ConvertTo-SecureString -AsPlainText $gitHubToken - AccessTokenType = $prefix - ApiBaseUri = $ApiBaseUri - ApiVersion = $ApiVersion - AuthType = 'IAT' - ClientID = $ClientID - HostName = $HostName - } - Set-GitHubConfig @settings - } - } - switch ($AuthType) { - 'App' { - $app = Get-GitHubApp - $username = $app.slug - } - 'IAT' { - $username = 'system' + if (-not $Silent) { + Write-Host '✓ ' -ForegroundColor Green -NoNewline + Write-Host "Logged in as $username!" } - default { - $user = Get-GitHubUser - $username = $user.login - } - } - if (-not $Silent) { - Write-Host '✓ ' -ForegroundColor Green -NoNewline - Write-Host "Logged in as $username!" - } + if ($Owner) { + Set-GitHubConfig -Owner $Owner + } - if ($Owner) { - Set-GitHubConfig -Owner $Owner - } + if ($Repo) { + Set-GitHubConfig -Repo $Repo + } - if ($Repo) { - Set-GitHubConfig -Repo $Repo + Remove-Variable -Name tokenResponse -ErrorAction SilentlyContinue + Remove-Variable -Name settings -ErrorAction SilentlyContinue + [System.GC]::Collect() + } catch { + throw $_ } - - Remove-Variable -Name tokenResponse -ErrorAction SilentlyContinue - Remove-Variable -Name settings -ErrorAction SilentlyContinue - [System.GC]::Collect() - } diff --git a/src/functions/public/Config/Set-GitHubConfig.ps1 b/src/functions/public/Config/Set-GitHubConfig.ps1 index ae346f9eb..6f62023cb 100644 --- a/src/functions/public/Config/Set-GitHubConfig.ps1 +++ b/src/functions/public/Config/Set-GitHubConfig.ps1 @@ -50,6 +50,8 @@ function Set-GitHubConfig { [string] $AuthType, # Set the client ID. + [AllowNull()] + [AllowEmptyString()] [string] $ClientID, # Set the device flow type. diff --git a/tests/Commands.Tests.ps1 b/tests/Commands.Tests.ps1 deleted file mode 100644 index 92dae9282..000000000 --- a/tests/Commands.Tests.ps1 +++ /dev/null @@ -1,21 +0,0 @@ -Describe 'Commands' { - It "Start-LogGroup 'MyGroup' should not throw" { - { - Start-LogGroup 'MyGroup' - } | Should -Not -Throw - } - - It 'Stop-LogGroup should not throw' { - { - Stop-LogGroup - } | Should -Not -Throw - } - - It "LogGroup 'MyGroup' should not throw" { - { - LogGroup 'MyGroup' { - Get-ChildItem env: | Select-Object Name, Value | Format-Table -AutoSize - } - } | Should -Not -Throw - } -} diff --git a/tests/GitHub.Tests.ps1 b/tests/GitHub.Tests.ps1 index 94f3d1f97..c08de14b6 100644 --- a/tests/GitHub.Tests.ps1 +++ b/tests/GitHub.Tests.ps1 @@ -1,8 +1,7 @@ -BeforeAll { - Connect-GitHub -} - -Describe 'GitHub' { +Describe 'GitHub' { + Context 'Connect-GitHub' { + { Connect-GitHub } | Should -Not -Throw + } Context 'Invoke-GitHubAPI' { It 'Invoke-GitHubAPI function exists' { Get-Command Invoke-GitHubAPI | Should -Not -BeNullOrEmpty @@ -29,3 +28,25 @@ Describe 'GitHub' { } } } + +Describe 'Commands' { + It "Start-LogGroup 'MyGroup' should not throw" { + { + Start-LogGroup 'MyGroup' + } | Should -Not -Throw + } + + It 'Stop-LogGroup should not throw' { + { + Stop-LogGroup + } | Should -Not -Throw + } + + It "LogGroup 'MyGroup' should not throw" { + { + LogGroup 'MyGroup' { + Get-ChildItem env: | Select-Object Name, Value | Format-Table -AutoSize + } + } | Should -Not -Throw + } +}