From 09be1b5348446b2248e22050cacdb9b70cf8c0d1 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 17 Jul 2025 14:37:01 +0200 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=9A=80=20[Feature]:=20Rename=20`RunSt?= =?UTF-8?q?artedAt`=20attribute=20to=20`StartedAt`=20for=20workflow=20runs?= =?UTF-8?q?=20+=20example=20update=20(#480)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This pull request refactors PowerShell scripts for managing GitHub apps and updates a property name in the `GitHubWorkflowRun` class for consistency. The changes focus on improving code clarity, standardizing naming conventions, and enhancing maintainability. ### 🌟 Updates to `GitHubWorkflowRun` class: * Renamed the `RunStartedAt` property to `StartedAt` for consistency with other properties. * Updated the constructor to reflect the property name change from `RunStartedAt` to `StartedAt`. ### Refactoring of GitHub app management scripts: * Removed the `examples/Apps/AppManagement.ps1` example entirely, consolidating its functionality into `examples/Apps/EnterpriseApps.ps1` for better organization. * Updated `examples/Apps/EnterpriseApps.ps1` to: - Replace hardcoded app IDs with a more flexible `$ClientIDs` array. - Introduce parameters for private key and client ID authentication (`$PrivateKey` and `$ClientID`). - Simplify organization filtering by using `$org.Name` instead of `$org.login`. ## Type of change - [ ] 📖 [Docs] - [ ] 🪲 [Fix] - [ ] 🩹 [Patch] - [ ] ⚠️ [Security fix] - [x] 🚀 [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 --- examples/Apps/AppManagement.ps1 | 10 ---- examples/Apps/EnterpriseApps.ps1 | 53 +++++++------------ .../public/Workflows/GitHubWorkflowRun.ps1 | 4 +- 3 files changed, 20 insertions(+), 47 deletions(-) delete mode 100644 examples/Apps/AppManagement.ps1 diff --git a/examples/Apps/AppManagement.ps1 b/examples/Apps/AppManagement.ps1 deleted file mode 100644 index 677f9e0c3..000000000 --- a/examples/Apps/AppManagement.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -# Install an app on the entire enterprise -$appIDs = @( - 'Iv1.f26b61bc99e69405' -) -$orgs = Get-GitHubOrganization -Enterprise 'msx' -foreach ($org in $orgs) { - foreach ($appID in $appIDs) { - Install-GitHubAppOnEnterpriseOrganization -Enterprise msx -Organization $org.login -ClientID $appID -RepositorySelection all - } -} diff --git a/examples/Apps/EnterpriseApps.ps1 b/examples/Apps/EnterpriseApps.ps1 index 2526b7b2d..eed2507df 100644 --- a/examples/Apps/EnterpriseApps.ps1 +++ b/examples/Apps/EnterpriseApps.ps1 @@ -1,40 +1,23 @@ -$appIDs = @( - 'qweqweqwe', - 'qweqweqweqwe' -) - -$organization = '*' -filter Install-GithubApp { - param( - [Parameter()] - [string] $Enterprise = 'msx', +$ClientID = '' +$PrivateKey = @' +-----BEGIN RSA PRIVATE KEY----- - [Parameter()] - [string] $Organization = '*', +-----END RSA PRIVATE KEY----- +'@ +Connect-GitHub -ClientID $ClientID -PrivateKey $PrivateKey +Connect-GitHubApp -Enterprise 'msx' - [Parameter( - Mandatory, - ValueFromPipeline - )] - [string] $AppID - ) +# The apps you want to install on orgs in the enterprise +$ClientIDs = @( + 'Iv1.f26b61bc99e69405' +) +$Enterprise = 'msx' +$Organization = '*' - process { - $installableOrgs = Get-GitHubOrganization -Enterprise $Enterprise - $orgs = $installableOrgs | Where-Object { $_.login -like $organization } - foreach ($org in $orgs) { - foreach ($appIDitem in $AppID) { - Install-GitHubApp -Enterprise $Enterprise -Organization $org.login -ClientID $appIDitem -RepositorySelection all | ForEach-Object { - [PSCustomObject]@{ - Organization = $org.login - AppID = $appIDitem - } - } - } - } +$installableOrgs = Get-GitHubOrganization -Enterprise $Enterprise +$orgs = $installableOrgs | Where-Object { $_.Name -like $Organization } +foreach ($org in $orgs) { + foreach ($ClientID in $ClientIDs) { + Install-GitHubApp -Enterprise $Enterprise -Organization $org.Name -ClientID $ClientID -RepositorySelection all } } - -$appIDs | Install-GitHubApp -Organization $organization - -$installation = Get-GitHubAppInstallation diff --git a/src/classes/public/Workflows/GitHubWorkflowRun.ps1 b/src/classes/public/Workflows/GitHubWorkflowRun.ps1 index 6a6e60e18..223434062 100644 --- a/src/classes/public/Workflows/GitHubWorkflowRun.ps1 +++ b/src/classes/public/Workflows/GitHubWorkflowRun.ps1 @@ -85,7 +85,7 @@ # The start time of the latest run. Resets on re-run. # Example: "2023-01-01T12:01:00Z" - [System.Nullable[datetime]] $RunStartedAt + [System.Nullable[datetime]] $StartedAt # The head commit details. # Example: (nullable-simple-commit object) @@ -126,7 +126,7 @@ $this.PullRequests = $_.pull_requests $this.CreatedAt = $_.created_at $this.UpdatedAt = $_.updated_at - $this.RunStartedAt = $_.run_started_at + $this.StartedAt = $_.run_started_at $this.Actor = [GitHubUser]::new($_.actor) $this.TriggeringActor = [GitHubUser]::new($_.triggering_actor) $this.HeadCommit = $_.head_commit From 465671b848af7d299501cbde04149dd3026667e4 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 18 Jul 2025 15:12:53 +0200 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=9A=80=20[Feature]:=20Adding=20functi?= =?UTF-8?q?onality=20to=20sign=20JWTs=20via=20Key=20Vault=20Keys=20(#481)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This pull request introduces support for signing GitHub App JSON Web Tokens (JWTs) using Azure Key Vault in addition to local RSA private keys. It also refactors and enhances existing JWT-related functionality to improve maintainability and clarity. The most significant changes include the addition of Azure Key Vault integration, refactoring of JWT signing methods, and updates to related utility functions. - Fixes #470. ### Improvements to Authentication Logic * Enhanced `Connect-GitHubAccount` to support both private key and Azure Key Vault-based authentication for GitHub Apps, introducing new parameter sets and validation for `KeyVaultKeyReference`. ### Azure Key Vault Integration * Added a new `KeyVaultKeyReference` property to the `GitHubAppContext` class for specifying Azure Key Vault keys as an alternative to local private keys. * Introduced the `Add-GitHubKeyVaultJWTSignature` function to sign JWTs using Azure Key Vault keys, supporting both Azure CLI and Az PowerShell authentication. * Added utility functions `Test-GitHubAzureCLI` and `Test-GitHubAzPowerShell` to check for Azure CLI and Az PowerShell module installation and authentication. ### Refactoring of JWT Signing * Renamed `Add-GitHubJWTSignature` to `Add-GitHubLocalJWTSignature` for clarity and updated it to use the new `GitHubJWTComponent` helper for base64 URL encoding. * Updated `Update-GitHubAppJWT` to conditionally use either `Add-GitHubLocalJWTSignature` or `Add-GitHubKeyVaultJWTSignature` based on the presence of `PrivateKey` or `KeyVaultKeyReference` in the context. ### Enhancements to JWT Utility Functions * Added `GitHubJWTComponent` class to centralize base64 URL encoding logic and simplify JWT creation. * Updated `New-GitHubUnsignedJWT` to use `GitHubJWTComponent` for encoding JWT headers and payloads. ## See it in action https://github.com/PSModule/GitHub-Script/actions/runs/16364842244/job/46239654619 ## Type of change - [ ] 📖 [Docs] - [ ] 🪲 [Fix] - [ ] 🩹 [Patch] - [ ] ⚠️ [Security fix] - [x] 🚀 [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 --- README.md | 42 ++++++++ .../GitHubContext/GitHubAppContext.ps1 | 4 + src/classes/public/GitHubJWTComponent.ps1 | 15 +++ .../Add-GitHubKeyVaultJWTSignature.ps1 | 101 ++++++++++++++++++ ...re.ps1 => Add-GitHubLocalJWTSignature.ps1} | 29 ++--- .../GitHub Apps/New-GitHubUnsignedJWT.ps1 | 36 +++---- .../Test-GitHubJWTRefreshRequired.ps1 | 43 ++++++++ .../Apps/GitHub Apps/Update-GitHubAppJWT.ps1 | 92 +++++++++++++--- .../Auth/Context/Resolve-GitHubContext.ps1 | 3 +- .../Test-GitHubAccessTokenRefreshRequired.ps1 | 7 +- .../Update-GitHubUserAccessToken.ps1 | 12 +-- .../PowerShell/Test-GitHubAzPowerShell.ps1 | 66 ++++++++++++ .../PowerShell/Test-GitHubAzureCLI.ps1 | 65 +++++++++++ .../public/Auth/Connect-GitHubAccount.ps1 | 77 ++++++++----- src/variables/private/GitHub.ps1 | 2 +- tests/Apps.Tests.ps1 | 2 +- 16 files changed, 504 insertions(+), 92 deletions(-) create mode 100644 src/classes/public/GitHubJWTComponent.ps1 create mode 100644 src/functions/private/Apps/GitHub Apps/Add-GitHubKeyVaultJWTSignature.ps1 rename src/functions/private/Apps/GitHub Apps/{Add-GitHubJWTSignature.ps1 => Add-GitHubLocalJWTSignature.ps1} (64%) create mode 100644 src/functions/private/Apps/GitHub Apps/Test-GitHubJWTRefreshRequired.ps1 create mode 100644 src/functions/private/Utilities/PowerShell/Test-GitHubAzPowerShell.ps1 create mode 100644 src/functions/private/Utilities/PowerShell/Test-GitHubAzureCLI.ps1 diff --git a/README.md b/README.md index f8286fb13..583424976 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,48 @@ Connect-GitHubAccount -ClientID $ClientID -PrivateKey $PrivateKey Using this approach, the module will autogenerate a JWT every time you run a command. I.e. Get-GitHubApp. +#### Using a GitHub App with Azure Key Vault + +For enhanced security, you can store your GitHub App's keys in Azure Key Vault and use that as way to signing the JWTs. +This approach requires a pre-authenticated session with either Azure CLI or Azure PowerShell. + +**Prerequisites:** +- Azure CLI authenticated session (`az login`) or Azure PowerShell authenticated session (`Connect-AzAccount`) +- GitHub App private key stored as a key in Azure Key Vault, with 'Sign' as a permitted operation +- Appropriate permissions to read keys from the Key Vault, like 'Key Vault Crypto User' + +**Using Azure CLI authentication:** + +```powershell +# Ensure you're authenticated with Azure CLI +az login + +# Connect using Key Vault key reference (URI with or without version) +Connect-GitHubAccount -ClientID $ClientID -KeyVaultKeyReference 'https://my-keyvault.vault.azure.net/keys/github-app-private-key' +✓ Logged in as my-github-app! +``` + +**Using Azure PowerShell authentication:** + +```powershell +# Ensure you're authenticated with Azure PowerShell +Connect-AzAccount + +# Connect using Key Vault key reference (URI with or without version) +Connect-GitHubAccount -ClientID $ClientID -KeyVaultKeyReference 'https://my-keyvault.vault.azure.net/keys/github-app-private-key' +✓ Logged in as my-github-app! +``` + +**Using Key Vault key reference with version:** + +```powershell +# Connect using Key Vault key reference with specific version +Connect-GitHubAccount -ClientID $ClientID -KeyVaultKeyReference 'https://my-keyvault.vault.azure.net/keys/github-app-private-key/abc123def456' +✓ Logged in as my-github-app! +``` + +This method ensures that your private key is securely stored in Azure Key Vault and never exposed in your scripts or configuration files. + #### Using a different host If you are using GitHub Enterprise, you can use the `-Host` (or `-HostName`) parameter to specify the host you want to connect to. diff --git a/src/classes/public/Context/GitHubContext/GitHubAppContext.ps1 b/src/classes/public/Context/GitHubContext/GitHubAppContext.ps1 index 6f9995e5b..02cc68a67 100644 --- a/src/classes/public/Context/GitHubContext/GitHubAppContext.ps1 +++ b/src/classes/public/Context/GitHubContext/GitHubAppContext.ps1 @@ -5,6 +5,9 @@ # The private key for the app. [securestring] $PrivateKey + # Azure Key Vault key reference for JWT signing (alternative to PrivateKey). + [string] $KeyVaultKeyReference + # Owner of the GitHub App [string] $OwnerName @@ -41,6 +44,7 @@ $this.PerPage = $Object.PerPage $this.ClientID = $Object.ClientID $this.PrivateKey = $Object.PrivateKey + $this.KeyVaultKeyReference = $Object.KeyVaultKeyReference $this.OwnerName = $Object.OwnerName $this.OwnerType = $Object.OwnerType $this.Permissions = $Object.Permissions diff --git a/src/classes/public/GitHubJWTComponent.ps1 b/src/classes/public/GitHubJWTComponent.ps1 new file mode 100644 index 000000000..2f24a1e55 --- /dev/null +++ b/src/classes/public/GitHubJWTComponent.ps1 @@ -0,0 +1,15 @@ +class GitHubJWTComponent { + static [string] ToBase64UrlString([hashtable] $Data) { + return [GitHubJWTComponent]::ConvertToBase64UrlFormat( + [System.Convert]::ToBase64String( + [System.Text.Encoding]::UTF8.GetBytes( + (ConvertTo-Json -InputObject $Data) + ) + ) + ) + } + + static [string] ConvertToBase64UrlFormat([string] $Base64String) { + return $Base64String.TrimEnd('=').Replace('+', '-').Replace('/', '_') + } +} diff --git a/src/functions/private/Apps/GitHub Apps/Add-GitHubKeyVaultJWTSignature.ps1 b/src/functions/private/Apps/GitHub Apps/Add-GitHubKeyVaultJWTSignature.ps1 new file mode 100644 index 000000000..1665a309c --- /dev/null +++ b/src/functions/private/Apps/GitHub Apps/Add-GitHubKeyVaultJWTSignature.ps1 @@ -0,0 +1,101 @@ +function Add-GitHubKeyVaultJWTSignature { + <# + .SYNOPSIS + Adds a JWT signature using Azure Key Vault. + + .DESCRIPTION + Signs an unsigned JWT (header.payload) using a key stored in Azure Key Vault. + The function supports authentication via Azure CLI or Az PowerShell module and returns the signed JWT as a secure string. + + .EXAMPLE + Add-GitHubKeyVaultJWTSignature -UnsignedJWT 'header.payload' -KeyVaultKeyReference 'https://myvault.vault.azure.net/keys/mykey' + + Output: + ```powershell + System.Security.SecureString + ``` + + Signs the provided JWT (`header.payload`) using the specified Azure Key Vault key, returning a secure string containing the signed JWT. + + .OUTPUTS + System.Security.SecureString + + .NOTES + The function returns a secure string containing the fully signed JWT (header.payload.signature). + Ensure Azure CLI or Az PowerShell is installed and authenticated before running this function. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingConvertToSecureStringWithPlainText', '', + Justification = 'Used to handle secure string private keys.' + )] + [CmdletBinding()] + param ( + # The unsigned JWT (header.payload) to sign. + [Parameter(Mandatory)] + [string] $UnsignedJWT, + + # The Azure Key Vault key URL used for signing. + [Parameter(Mandatory)] + [string] $KeyVaultKeyReference + ) + + begin { + $stackPath = Get-PSCallStackPath + Write-Debug "[$stackPath] - Start" + } + + process { + if (Test-GitHubAzureCLI) { + try { + $accessToken = (az account get-access-token --resource 'https://vault.azure.net/' --output json | ConvertFrom-Json).accessToken + } catch { + Write-Error "Failed to get access token from Azure CLI: $_" + return + } + } elseif (Test-GitHubAzPowerShell) { + try { + $accessToken = (Get-AzAccessToken -ResourceUrl 'https://vault.azure.net/').Token + } catch { + Write-Error "Failed to get access token from Az PowerShell: $_" + return + } + } else { + Write-Error 'Azure authentication is required. Please ensure you are logged in using either Azure CLI or Az PowerShell.' + return + } + + if ($accessToken -isnot [securestring]) { + $accessToken = ConvertTo-SecureString -String $accessToken -AsPlainText + } + + $hash64url = [GitHubJWTComponent]::ConvertToBase64UrlFormat( + [System.Convert]::ToBase64String( + [System.Security.Cryptography.SHA256]::Create().ComputeHash( + [System.Text.Encoding]::UTF8.GetBytes($UnsignedJWT) + ) + ) + ) + + $KeyVaultKeyReference = $KeyVaultKeyReference.TrimEnd('/') + + $params = @{ + Method = 'POST' + URI = "$KeyVaultKeyReference/sign?api-version=7.4" + Body = @{ + alg = 'RS256' + value = $hash64url + } | ConvertTo-Json + ContentType = 'application/json' + Authentication = 'Bearer' + Token = $accessToken + } + + $result = Invoke-RestMethod @params + $signature = $result.value + return (ConvertTo-SecureString -String "$UnsignedJWT.$signature" -AsPlainText) + } + + end { + Write-Debug "[$stackPath] - End" + } +} diff --git a/src/functions/private/Apps/GitHub Apps/Add-GitHubJWTSignature.ps1 b/src/functions/private/Apps/GitHub Apps/Add-GitHubLocalJWTSignature.ps1 similarity index 64% rename from src/functions/private/Apps/GitHub Apps/Add-GitHubJWTSignature.ps1 rename to src/functions/private/Apps/GitHub Apps/Add-GitHubLocalJWTSignature.ps1 index f00d160da..1c14e89f3 100644 --- a/src/functions/private/Apps/GitHub Apps/Add-GitHubJWTSignature.ps1 +++ b/src/functions/private/Apps/GitHub Apps/Add-GitHubLocalJWTSignature.ps1 @@ -1,4 +1,4 @@ -function Add-GitHubJWTSignature { +function Add-GitHubLocalJWTSignature { <# .SYNOPSIS Signs a JSON Web Token (JWT) using a local RSA private key. @@ -8,26 +8,25 @@ function Add-GitHubJWTSignature { This function handles the RSA signing process and returns the complete signed JWT. .EXAMPLE - Add-GitHubJWTSignature -UnsignedJWT 'eyJ0eXAiOi...' -PrivateKey '--- BEGIN RSA PRIVATE KEY --- ... --- END RSA PRIVATE KEY ---' + Add-GitHubLocalJWTSignature -UnsignedJWT 'eyJ0eXAiOi...' -PrivateKey '--- BEGIN RSA PRIVATE KEY --- ... --- END RSA PRIVATE KEY ---' Adds a signature to the unsigned JWT using the provided private key. .OUTPUTS - String + securestring .NOTES This function isolates the signing logic to enable support for multiple signing methods. .LINK - https://psmodule.io/GitHub/Functions/Apps/GitHub%20App/Add-GitHubJWTSignature + https://psmodule.io/GitHub/Functions/Apps/GitHub%20App/Add-GitHubLocalJWTSignature #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSAvoidUsingConvertToSecureStringWithPlainText', - '', + 'PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'Used to handle secure string private keys.' )] [CmdletBinding()] - [OutputType([string])] + [OutputType([securestring])] param( # The unsigned JWT (header.payload) to sign. [Parameter(Mandatory)] @@ -52,14 +51,16 @@ function Add-GitHubJWTSignature { $rsa.ImportFromPem($PrivateKey) try { - $signature = [Convert]::ToBase64String( - $rsa.SignData( - [System.Text.Encoding]::UTF8.GetBytes($UnsignedJWT), - [System.Security.Cryptography.HashAlgorithmName]::SHA256, - [System.Security.Cryptography.RSASignaturePadding]::Pkcs1 + $signature = [GitHubJWTComponent]::ConvertToBase64UrlFormat( + [System.Convert]::ToBase64String( + $rsa.SignData( + [System.Text.Encoding]::UTF8.GetBytes($UnsignedJWT), + [System.Security.Cryptography.HashAlgorithmName]::SHA256, + [System.Security.Cryptography.RSASignaturePadding]::Pkcs1 + ) ) - ).TrimEnd('=').Replace('+', '-').Replace('/', '_') - return "$UnsignedJWT.$signature" + ) + return (ConvertTo-SecureString -String "$UnsignedJWT.$signature" -AsPlainText) } finally { if ($rsa) { $rsa.Dispose() diff --git a/src/functions/private/Apps/GitHub Apps/New-GitHubUnsignedJWT.ps1 b/src/functions/private/Apps/GitHub Apps/New-GitHubUnsignedJWT.ps1 index 5332505c5..2d47a9f81 100644 --- a/src/functions/private/Apps/GitHub Apps/New-GitHubUnsignedJWT.ps1 +++ b/src/functions/private/Apps/GitHub Apps/New-GitHubUnsignedJWT.ps1 @@ -1,4 +1,4 @@ -function New-GitHubUnsignedJWT { +function New-GitHubUnsignedJWT { <# .SYNOPSIS Creates an unsigned JSON Web Token (JWT) for a GitHub App. @@ -38,30 +38,22 @@ function New-GitHubUnsignedJWT { } process { - $header = [Convert]::ToBase64String( - [System.Text.Encoding]::UTF8.GetBytes( - ( - ConvertTo-Json -InputObject @{ - alg = 'RS256' - typ = 'JWT' - } - ) - ) - ).TrimEnd('=').Replace('+', '-').Replace('/', '_') + $header = [GitHubJWTComponent]::ToBase64UrlString( + @{ + alg = 'RS256' + typ = 'JWT' + } + ) $now = [System.DateTimeOffset]::UtcNow $iat = $now.AddSeconds(-$script:GitHub.Config.JwtTimeTolerance) $exp = $now.AddSeconds($script:GitHub.Config.JwtTimeTolerance) - $payload = [Convert]::ToBase64String( - [System.Text.Encoding]::UTF8.GetBytes( - ( - ConvertTo-Json -InputObject @{ - iat = $iat.ToUnixTimeSeconds() - exp = $exp.ToUnixTimeSeconds() - iss = $ClientID - } - ) - ) - ).TrimEnd('=').Replace('+', '-').Replace('/', '_') + $payload = [GitHubJWTComponent]::ToBase64UrlString( + @{ + iat = $iat.ToUnixTimeSeconds() + exp = $exp.ToUnixTimeSeconds() + iss = $ClientID + } + ) [pscustomobject]@{ Base = "$header.$payload" IssuedAt = $iat.DateTime diff --git a/src/functions/private/Apps/GitHub Apps/Test-GitHubJWTRefreshRequired.ps1 b/src/functions/private/Apps/GitHub Apps/Test-GitHubJWTRefreshRequired.ps1 new file mode 100644 index 000000000..ae705ead3 --- /dev/null +++ b/src/functions/private/Apps/GitHub Apps/Test-GitHubJWTRefreshRequired.ps1 @@ -0,0 +1,43 @@ +function Test-GitHubJWTRefreshRequired { + <# + .SYNOPSIS + Test if the GitHub JWT should be refreshed. + + .DESCRIPTION + Test if the GitHub JWT should be refreshed. JWTs are refreshed when they have 150 seconds or less remaining before expiration. + + .EXAMPLE + Test-GitHubJWTRefreshRequired -Context $Context + + This will test if the GitHub JWT should be refreshed for the specified context. + + .NOTES + JWTs are short-lived tokens (typically 10 minutes) and need to be refreshed more frequently than user access tokens. + The refresh threshold is set to 150 seconds (2.5 minutes) to ensure the JWT doesn't expire during API operations. + #> + [OutputType([bool])] + [CmdletBinding()] + param( + # 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. + [Parameter(Mandatory)] + [object] $Context + ) + + begin { + $stackPath = Get-PSCallStackPath + Write-Debug "[$stackPath] - Start" + } + + process { + try { + ($Context.TokenExpiresAt - [datetime]::Now).TotalSeconds -le ($script:GitHub.Config.JwtTimeTolerance / 2) + } catch { + return $true + } + } + + end { + Write-Debug "[$stackPath] - End" + } +} diff --git a/src/functions/private/Apps/GitHub Apps/Update-GitHubAppJWT.ps1 b/src/functions/private/Apps/GitHub Apps/Update-GitHubAppJWT.ps1 index de4ce05d4..350a47434 100644 --- a/src/functions/private/Apps/GitHub Apps/Update-GitHubAppJWT.ps1 +++ b/src/functions/private/Apps/GitHub Apps/Update-GitHubAppJWT.ps1 @@ -4,15 +4,26 @@ Updates a JSON Web Token (JWT) for a GitHub App context. .DESCRIPTION - Updates a JSON Web Token (JWT) for a GitHub App context. + Updates a JSON Web Token (JWT) for a GitHub App context. If the JWT has half or less of its remaining duration before expiration, + it will be refreshed. This function implements mutex-based locking to prevent concurrent refreshes. .EXAMPLE Update-GitHubAppJWT -Context $Context Updates the JSON Web Token (JWT) for a GitHub App using the specified context. + .EXAMPLE + Update-GitHubAppJWT -Context $Context -PassThru + + This will update the GitHub App JWT for the specified context and return the updated context. + + .EXAMPLE + Update-GitHubAppJWT -Context $Context -Silent + + This will update the GitHub App JWT for the specified context without displaying progress messages. + .OUTPUTS - securestring + object .NOTES [Generating a JSON Web Token (JWT) for a GitHub App | GitHub Docs](https://docs.github.com/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app#example-using-powershell-to-generate-a-jwt) @@ -20,6 +31,8 @@ .LINK https://psmodule.io/GitHub/Functions/Apps/GitHub%20App/Update-GitHubAppJWT #> + [CmdletBinding(SupportsShouldProcess)] + [OutputType([object])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSAvoidLongLines', '', Justification = 'Contains a long link.' @@ -28,12 +41,14 @@ 'PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'Generated JWT is a plaintext string.' )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingWriteHost', '', + Justification = 'Is the CLI part of the module.' + )] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function creates a JWT without modifying system state' )] - [CmdletBinding()] - [OutputType([object])] param( # 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. @@ -42,7 +57,11 @@ # Return the updated context. [Parameter()] - [switch] $PassThru + [switch] $PassThru, + + # Timeout in milliseconds for waiting on mutex. Default is 30 seconds. + [Parameter()] + [int] $TimeoutMs = 30000 ) begin { @@ -51,15 +70,64 @@ } process { - $unsignedJWT = New-GitHubUnsignedJWT -ClientId $Context.ClientID - $jwt = Add-GitHubJWTSignature -UnsignedJWT $unsignedJWT.Base -PrivateKey $Context.PrivateKey - $Context.Token = ConvertTo-SecureString -String $jwt -AsPlainText - $Context.TokenExpiresAt = $unsignedJWT.ExpiresAt - if ($Context.ID) { - $Context = Set-Context -Context $Context -Vault $script:GitHub.ContextVault -PassThru + if (Test-GitHubJWTRefreshRequired -Context $Context) { + $lockName = "PSModule.GitHub-$($Context.ID)".Replace('/', '-') + $lock = $null + try { + $lock = [System.Threading.Mutex]::new($false, $lockName) + $acquiredLock = $lock.WaitOne(0) + + if ($acquiredLock) { + try { + Write-Debug '⚠ JWT token nearing expiration. Refreshing JWT...' + $unsignedJWT = New-GitHubUnsignedJWT -ClientId $Context.ClientID + + if ($Context.KeyVaultKeyReference) { + Write-Debug "Using KeyVault Key Reference: $($Context.KeyVaultKeyReference)" + $Context.Token = Add-GitHubKeyVaultJWTSignature -UnsignedJWT $unsignedJWT.Base -KeyVaultKeyReference $Context.KeyVaultKeyReference + } elseif ($Context.PrivateKey) { + Write-Debug 'Using Private Key from context.' + $Context.Token = Add-GitHubLocalJWTSignature -UnsignedJWT $unsignedJWT.Base -PrivateKey $Context.PrivateKey + } else { + throw 'No Private Key or KeyVault Key Reference provided in the context.' + } + + $Context.TokenExpiresAt = $unsignedJWT.ExpiresAt + + if ($Context.ID) { + if ($PSCmdlet.ShouldProcess('JWT token', 'Update/refresh')) { + Set-Context -Context $Context -Vault $script:GitHub.ContextVault + } + } + } finally { + $lock.ReleaseMutex() + } + } else { + Write-Verbose "JWT token is being updated by another process. Waiting for mutex to be released (timeout: $($TimeoutMs)ms)..." + try { + if ($lock.WaitOne($TimeoutMs)) { + $Context = Resolve-GitHubContext -Context $Context.ID + $lock.ReleaseMutex() + } else { + Write-Warning 'Timeout waiting for JWT token update. Proceeding with current token state.' + } + } catch [System.Threading.AbandonedMutexException] { + Write-Debug 'Mutex was abandoned by another process. Re-checking JWT token state...' + $Context = Resolve-GitHubContext -Context $Context.ID + } + } + } finally { + if ($lock) { + $lock.Dispose() + } + } + } else { + # JWT is still valid, no refresh needed + Write-Debug 'JWT is still valid, no refresh needed' } + if ($PassThru) { - $Context + return $Context } } diff --git a/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 b/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 index a089f6add..a28e871a6 100644 --- a/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 +++ b/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 @@ -41,7 +41,8 @@ } process { - Write-Verbose "Context: [$Context]" + Write-Verbose "Context:" + $Context | Out-String -Stream | ForEach-Object { Write-Verbose $_ } Write-Verbose "Anonymous: [$Anonymous]" if ($Anonymous -or $Context -eq 'Anonymous') { Write-Verbose 'Returning Anonymous context.' diff --git a/src/functions/private/Auth/DeviceFlow/Test-GitHubAccessTokenRefreshRequired.ps1 b/src/functions/private/Auth/DeviceFlow/Test-GitHubAccessTokenRefreshRequired.ps1 index d9320a85d..292ded46a 100644 --- a/src/functions/private/Auth/DeviceFlow/Test-GitHubAccessTokenRefreshRequired.ps1 +++ b/src/functions/private/Auth/DeviceFlow/Test-GitHubAccessTokenRefreshRequired.ps1 @@ -26,8 +26,11 @@ } process { - $updateToken = ($Context.TokenExpiresAt - [datetime]::Now).TotalHours -lt $script:GitHub.Config.AccessTokenGracePeriodInHours - $updateToken + try { + ($Context.TokenExpiresAt - [datetime]::Now).TotalHours -lt $script:GitHub.Config.AccessTokenGracePeriodInHours + } catch { + return $true + } } end { diff --git a/src/functions/private/Auth/DeviceFlow/Update-GitHubUserAccessToken.ps1 b/src/functions/private/Auth/DeviceFlow/Update-GitHubUserAccessToken.ps1 index 4f4ce2ffe..ab5183e7e 100644 --- a/src/functions/private/Auth/DeviceFlow/Update-GitHubUserAccessToken.ps1 +++ b/src/functions/private/Auth/DeviceFlow/Update-GitHubUserAccessToken.ps1 @@ -43,10 +43,6 @@ [Parameter()] [switch] $PassThru, - # Suppress output messages. - [Parameter()] - [switch] $Silent, - # Timeout in milliseconds for waiting on mutex. Default is 30 seconds. [Parameter()] [int] $TimeoutMs = 30000 @@ -55,12 +51,11 @@ begin { $stackPath = Get-PSCallStackPath Write-Debug "[$stackPath] - Start" - Assert-GitHubContext -Context $Context -AuthType UAT } process { if (Test-GitHubAccessTokenRefreshRequired -Context $Context) { - $lockName = "PSModule.GitHub/$($Context.ID)" + $lockName = "PSModule.GitHub-$($Context.ID)".Replace('/', '-') $lock = $null try { $lock = [System.Threading.Mutex]::new($false, $lockName) @@ -71,10 +66,7 @@ $refreshTokenValidity = [datetime]($Context.RefreshTokenExpiresAt) - [datetime]::Now $refreshTokenIsValid = $refreshTokenValidity.TotalSeconds -gt 0 if ($refreshTokenIsValid) { - if (-not $Silent) { - Write-Host '⚠ ' -ForegroundColor Yellow -NoNewline - Write-Host 'Access token expired. Refreshing access token...' - } + Write-Debug '⚠ Access token expired. Refreshing access token...' $tokenResponse = Invoke-GitHubDeviceFlowLogin -ClientID $Context.AuthClientID -RefreshToken $Context.RefreshToken -HostName $Context.HostName } else { Write-Verbose "Using $($Context.DeviceFlowType) authentication..." diff --git a/src/functions/private/Utilities/PowerShell/Test-GitHubAzPowerShell.ps1 b/src/functions/private/Utilities/PowerShell/Test-GitHubAzPowerShell.ps1 new file mode 100644 index 000000000..54aa9cf2c --- /dev/null +++ b/src/functions/private/Utilities/PowerShell/Test-GitHubAzPowerShell.ps1 @@ -0,0 +1,66 @@ +function Test-GitHubAzPowerShell { + <# + .SYNOPSIS + Tests if Azure PowerShell module is installed and authenticated. + + .DESCRIPTION + This function checks if the Azure PowerShell module (Az) is installed and the user is authenticated. + It verifies both the availability of the module and the authentication status. + + .EXAMPLE + Test-GitHubAzPowerShell + + Returns $true if Azure PowerShell module is installed and authenticated, $false otherwise. + + .OUTPUTS + [bool] + Returns $true if Azure PowerShell module is installed and authenticated, $false otherwise. + + .NOTES + This function is used internally by other GitHub module functions that require Azure PowerShell authentication, + such as Azure Key Vault operations for GitHub App JWT signing. + #> + [OutputType([bool])] + [CmdletBinding()] + param() + + begin { + $stackPath = Get-PSCallStackPath + Write-Debug "[$stackPath] - Start" + } + + process { + try { + # Check if Azure PowerShell module is installed + $azModule = Get-Module -Name 'Az.Accounts' -ListAvailable -ErrorAction SilentlyContinue + if (-not $azModule) { + Write-Debug "[$stackPath] - Azure PowerShell module (Az.Accounts) not found" + return $false + } + + # Check if the module is imported + $importedModule = Get-Module -Name 'Az.Accounts' -ErrorAction SilentlyContinue + if (-not $importedModule) { + Write-Debug "[$stackPath] - Attempting to import Az.Accounts module" + Import-Module -Name 'Az.Accounts' -ErrorAction SilentlyContinue + } + + # Check if user is authenticated by trying to get current context + $context = Get-AzContext -ErrorAction SilentlyContinue + if (-not $context -or [string]::IsNullOrEmpty($context.Account)) { + Write-Debug "[$stackPath] - Azure PowerShell authentication failed or no account logged in" + return $false + } + + Write-Debug "[$stackPath] - Azure PowerShell is installed and authenticated (Account: $($context.Account.Id))" + return $true + } catch { + Write-Debug "[$stackPath] - Error checking Azure PowerShell: $($_.Exception.Message)" + return $false + } + } + + end { + Write-Debug "[$stackPath] - End" + } +} diff --git a/src/functions/private/Utilities/PowerShell/Test-GitHubAzureCLI.ps1 b/src/functions/private/Utilities/PowerShell/Test-GitHubAzureCLI.ps1 new file mode 100644 index 000000000..19a6db1f1 --- /dev/null +++ b/src/functions/private/Utilities/PowerShell/Test-GitHubAzureCLI.ps1 @@ -0,0 +1,65 @@ +function Test-GitHubAzureCLI { + <# + .SYNOPSIS + Tests if Azure CLI is installed and authenticated. + + .DESCRIPTION + This function checks if Azure CLI (az) is installed and the user is authenticated. + It verifies both the availability of the CLI tool and the authentication status. + + .EXAMPLE + Test-GitHubAzureCLI + + Returns $true if Azure CLI is installed and authenticated, $false otherwise. + + .OUTPUTS + bool + + .NOTES + This function is used internally by other GitHub module functions that require Azure CLI authentication, + such as Azure Key Vault operations for GitHub App JWT signing. + #> + [OutputType([bool])] + [CmdletBinding()] + param() + + begin { + $stackPath = Get-PSCallStackPath + Write-Debug "[$stackPath] - Start" + } + + process { + try { + # Check if Azure CLI is installed + $azCommand = Get-Command -Name 'az' -ErrorAction SilentlyContinue + if (-not $azCommand) { + Write-Debug "[$stackPath] - Azure CLI (az) command not found" + return $false + } + + # Check if user is authenticated by trying to get account info + $accountInfo = az account show --output json 2>$null + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrEmpty($accountInfo)) { + Write-Debug "[$stackPath] - Azure CLI authentication failed or no account logged in" + return $false + } + + # Parse the account info to ensure it's valid + $account = $accountInfo | ConvertFrom-Json -ErrorAction SilentlyContinue + if (-not $account -or [string]::IsNullOrEmpty($account.id)) { + Write-Debug "[$stackPath] - Azure CLI account information is invalid" + return $false + } + + Write-Debug "[$stackPath] - Azure CLI is installed and authenticated (Account: $($account.id))" + return $true + } catch { + Write-Debug "[$stackPath] - Error checking Azure CLI: $($_.Exception.Message)" + return $false + } + } + + end { + Write-Debug "[$stackPath] - End" + } +} diff --git a/src/functions/public/Auth/Connect-GitHubAccount.ps1 b/src/functions/public/Auth/Connect-GitHubAccount.ps1 index c96affaa8..20816ce7d 100644 --- a/src/functions/public/Auth/Connect-GitHubAccount.ps1 +++ b/src/functions/public/Auth/Connect-GitHubAccount.ps1 @@ -48,9 +48,18 @@ #> [Alias('Connect-GitHub')] [OutputType([void])] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidLongLines', '', Justification = 'Long links for documentation.')] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Is the CLI part of the module.')] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'The tokens are received as clear text. Mitigating exposure by removing variables and performing garbage collection.')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidLongLines', '', + Justification = 'Long links for documentation.' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingWriteHost', '', + Justification = 'Is the CLI part of the module.' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingConvertToSecureStringWithPlainText', '', + Justification = 'The tokens are received as clear text. Mitigating exposure by removing variables and performing garbage collection.' + )] [CmdletBinding(DefaultParameterSetName = 'UAT')] param( # Choose between authentication methods, either OAuthApp or GitHubApp. @@ -69,41 +78,39 @@ # The user will be prompted to enter the token. - [Parameter( - Mandatory, - ParameterSetName = 'PAT' - )] + [Parameter(Mandatory, ParameterSetName = 'PAT')] [switch] $UseAccessToken, # An access token to use for authentication. Can be both a string or a SecureString. # Supports both personal access tokens (PAT) and GitHub App installation access tokens (IAT). # Example: 'ghp_1234567890abcdef' # Example: 'ghs_1234567890abcdef' - [Parameter( - Mandatory, - ParameterSetName = 'Token' - )] + [Parameter(Mandatory, ParameterSetName = 'Token')] [object] $Token, # The client ID for the GitHub App to use for authentication. [Parameter(ParameterSetName = 'UAT')] - [Parameter( - Mandatory, - ParameterSetName = 'App' - )] + [Parameter(Mandatory, ParameterSetName = 'GitHub App using a PrivateKey')] + [Parameter(Mandatory, ParameterSetName = 'GitHub App using a KeyVault Key Reference')] [string] $ClientID, - # The private key for the GitHub App when authenticating as a GitHub App. - [Parameter( - Mandatory, - ParameterSetName = 'App' - )] + # The private key that is used to sign JWTs for the GitHub App. + [Parameter(Mandatory, ParameterSetName = 'GitHub App using a PrivateKey')] [object] $PrivateKey, + # The KeyVault Key Reference that can sign JWTs for the GitHub App. + [Parameter(Mandatory, ParameterSetName = 'GitHub App using a KeyVault Key Reference')] + [ValidateScript({ + if ($_ -notlike 'https://*.vault.azure.net/keys/*') { + throw "Invalid Key Vault key reference format: $_" + } + return $true + })] + [string] $KeyVaultKeyReference, + # Automatically load installations for the GitHub App. - [Parameter( - ParameterSetName = 'App' - )] + [Parameter(ParameterSetName = 'GitHub App using a PrivateKey')] + [Parameter(ParameterSetName = 'GitHub App using a KeyVault Key Reference')] [switch] $AutoloadInstallations, # The default enterprise to use in commands. @@ -160,10 +167,9 @@ $ApiVersion = $script:GitHub.Config.ApiVersion $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') { + if ($script:IsGitHubActions -and $PSCmdlet.ParameterSetName -notin @('GitHub App using a PrivateKey', 'GitHub App using a KeyVault Key Reference')) { $customTokenProvided = -not [string]::IsNullOrEmpty($Token) $gitHubTokenPresent = Test-GitHubToken Write-Verbose "A token was provided: [$customTokenProvided]" @@ -181,7 +187,6 @@ HostName = [string]$HostName HttpVersion = [string]$httpVersion PerPage = [int]$perPage - AuthType = [string]$authType Enterprise = [string]$Enterprise Owner = [string]$Owner Repository = [string]$Repository @@ -189,7 +194,7 @@ $context | Format-Table | Out-String -Stream | ForEach-Object { Write-Verbose $_ } - switch ($authType) { + switch ($PSCmdlet.ParameterSetName) { 'UAT' { Write-Verbose 'Logging in using device flow...' if (-not [string]::IsNullOrEmpty($ClientID)) { @@ -218,6 +223,7 @@ switch ($Mode) { 'GitHubApp' { $context += @{ + AuthType = 'UAT' Token = ConvertTo-SecureString -AsPlainText $tokenResponse.access_token TokenExpiresAt = ([DateTime]::Now).AddSeconds($tokenResponse.expires_in) TokenType = $tokenResponse.access_token -replace $script:GitHub.TokenPrefixPattern @@ -230,6 +236,7 @@ } 'OAuthApp' { $context += @{ + AuthType = 'UAT' Token = ConvertTo-SecureString -AsPlainText $tokenResponse.access_token TokenType = $tokenResponse.access_token -replace $script:GitHub.TokenPrefixPattern AuthClientID = $authClientID @@ -244,17 +251,27 @@ } } } - 'App' { - Write-Verbose 'Logging in as a GitHub App...' + 'GitHub App using a PrivateKey' { + Write-Verbose 'Logging in as a GitHub App using PrivateKey...' if (-not($PrivateKey -is [System.Security.SecureString])) { $PrivateKey = $PrivateKey | ConvertTo-SecureString -AsPlainText } $context += @{ + AuthType = 'APP' PrivateKey = $PrivateKey TokenType = 'JWT' ClientID = $ClientID } } + 'GitHub App using a KeyVault Key Reference' { + Write-Verbose 'Logging in as a GitHub App using KeyVault Key Reference...' + $context += @{ + AuthType = 'APP' + KeyVaultKeyReference = $KeyVaultKeyReference + TokenType = 'JWT' + ClientID = $ClientID + } + } 'PAT' { Write-Debug "UseAccessToken is set to [$UseAccessToken]. Using provided access token..." Write-Verbose 'Logging in using personal access token...' @@ -264,6 +281,7 @@ $Token = ConvertFrom-SecureString $accessTokenValue -AsPlainText $tokenType = $Token -replace $script:GitHub.TokenPrefixPattern $context += @{ + AuthType = 'PAT' Token = ConvertTo-SecureString -AsPlainText $Token TokenType = $tokenType } @@ -294,6 +312,7 @@ } } } + default {} } $contextObj = Set-GitHubContext -Context $context -Default:(!$NotDefault) -PassThru $contextObj | Format-List | Out-String -Stream | ForEach-Object { Write-Verbose $_ } diff --git a/src/variables/private/GitHub.ps1 b/src/variables/private/GitHub.ps1 index 883105756..93d1d1344 100644 --- a/src/variables/private/GitHub.ps1 +++ b/src/variables/private/GitHub.ps1 @@ -1,5 +1,5 @@ $script:IsGitHubActions = $env:GITHUB_ACTIONS -eq 'true' -$script:IsFunctionApp = -not [string]::IsNullOrEmpty($env:WEBSITE_PLATFORM_VERSION) +$script:IsFunctionApp = $env:FUNCTIONS_WORKER_RUNTIME -eq 'powershell' $script:IsLocal = -not ($script:IsGitHubActions -or $script:IsFunctionApp) $script:GitHub = [pscustomobject]@{ ContextVault = 'PSModule.GitHub' diff --git a/tests/Apps.Tests.ps1 b/tests/Apps.Tests.ps1 index 8904b0a8c..44803729b 100644 --- a/tests/Apps.Tests.ps1 +++ b/tests/Apps.Tests.ps1 @@ -24,8 +24,8 @@ Describe 'Apps' { Context 'As using on ' -ForEach $authCases { BeforeAll { - $context = Connect-GitHubAccount @connectParams -PassThru -Silent LogGroup 'Context' { + $context = Connect-GitHubAccount @connectParams -PassThru -Silent Write-Host ($context | Format-List | Out-String) } } From 6b2e03eff622a0a6ad0c116083892f7f7a7889e8 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 19 Jul 2025 00:01:39 +0200 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=A9=B9=20[Patch]:=20Enhance=20`Test-G?= =?UTF-8?q?itHubWebhookSignature`=20to=20support=20a=20full=20request=20ob?= =?UTF-8?q?ject=20+=20`Context`=20bump=20(#482)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This pull request introduces several updates across multiple files, focusing on enhancing functionality, improving documentation, and updating dependencies. The most significant changes include updates to the `Test-GitHubWebhookSignature` function for better flexibility and security and the upgrade of required module versions. ### Functional Updates * `Test-GitHubWebhookSignature`: - Added support for validating webhook requests using the entire `Request` object, enabling automatic extraction of body and headers. - Updated descriptions to clarify the use of SHA-256 and added examples demonstrating validation with the `Request` object. ### Dependency Updates - Updated `#Requires` statements across multiple files to require version `8.1.1` of the `Context` module. The update fixes an issue where the GitHub module attempted to save a context with null values would throw a null-pointer exception. ### Test Enhancements - Expanded test coverage for `Test-GitHubWebhookSignature`, including scenarios for valid signatures, invalid signatures, and missing headers in the `Request` object. ## 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 --- .github/PSModule.yml | 14 ++++ .../Auth/Context/Remove-GitHubContext.ps1 | 2 +- .../Auth/Context/Set-GitHubContext.ps1 | 2 +- .../Config/Initialize-GitHubConfig.ps1 | 2 +- .../public/Auth/Context/Get-GitHubContext.ps1 | 2 +- .../public/Config/Get-GitHubConfig.ps1 | 2 +- .../public/Config/Remove-GitHubConfig.ps1 | 2 +- .../public/Config/Set-GitHubConfig.ps1 | 2 +- .../Webhooks/Test-GitHubWebhookSignature.ps1 | 83 ++++++++++++++----- tests/GitHub.Tests.ps1 | 81 ++++++++++++------ 10 files changed, 140 insertions(+), 52 deletions(-) diff --git a/.github/PSModule.yml b/.github/PSModule.yml index 6d578178e..0e0770314 100644 --- a/.github/PSModule.yml +++ b/.github/PSModule.yml @@ -1,3 +1,17 @@ Test: CodeCoverage: PercentTarget: 50 +# TestResults: +# Skip: true +# SourceCode: +# Skip: true +# PSModule: +# Skip: true +# Module: +# Windows: +# Skip: true +# MacOS: +# Skip: true +# Build: +# Docs: +# Skip: true diff --git a/src/functions/private/Auth/Context/Remove-GitHubContext.ps1 b/src/functions/private/Auth/Context/Remove-GitHubContext.ps1 index 9b6f810d8..e74e8aa5d 100644 --- a/src/functions/private/Auth/Context/Remove-GitHubContext.ps1 +++ b/src/functions/private/Auth/Context/Remove-GitHubContext.ps1 @@ -42,4 +42,4 @@ Write-Debug "[$stackPath] - End" } } -#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.0' } +#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' } diff --git a/src/functions/private/Auth/Context/Set-GitHubContext.ps1 b/src/functions/private/Auth/Context/Set-GitHubContext.ps1 index 068bafa99..b38085248 100644 --- a/src/functions/private/Auth/Context/Set-GitHubContext.ps1 +++ b/src/functions/private/Auth/Context/Set-GitHubContext.ps1 @@ -168,4 +168,4 @@ Write-Debug "[$stackPath] - End" } } -#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.0' } +#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' } diff --git a/src/functions/private/Config/Initialize-GitHubConfig.ps1 b/src/functions/private/Config/Initialize-GitHubConfig.ps1 index 8c513a8f5..3ca1bc34d 100644 --- a/src/functions/private/Config/Initialize-GitHubConfig.ps1 +++ b/src/functions/private/Config/Initialize-GitHubConfig.ps1 @@ -75,4 +75,4 @@ Write-Debug "[$stackPath] - End" } } -#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.0' } +#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' } diff --git a/src/functions/public/Auth/Context/Get-GitHubContext.ps1 b/src/functions/public/Auth/Context/Get-GitHubContext.ps1 index 083bf74ec..ccdd620ae 100644 --- a/src/functions/public/Auth/Context/Get-GitHubContext.ps1 +++ b/src/functions/public/Auth/Context/Get-GitHubContext.ps1 @@ -92,4 +92,4 @@ Write-Debug "[$stackPath] - End" } } -#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.0' } +#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' } diff --git a/src/functions/public/Config/Get-GitHubConfig.ps1 b/src/functions/public/Config/Get-GitHubConfig.ps1 index 9a2a30922..e8a0ee1bc 100644 --- a/src/functions/public/Config/Get-GitHubConfig.ps1 +++ b/src/functions/public/Config/Get-GitHubConfig.ps1 @@ -47,4 +47,4 @@ Write-Debug "[$stackPath] - End" } } -#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.0' } +#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' } diff --git a/src/functions/public/Config/Remove-GitHubConfig.ps1 b/src/functions/public/Config/Remove-GitHubConfig.ps1 index 148e3f264..e432b7044 100644 --- a/src/functions/public/Config/Remove-GitHubConfig.ps1 +++ b/src/functions/public/Config/Remove-GitHubConfig.ps1 @@ -44,4 +44,4 @@ Write-Debug "[$stackPath] - End" } } -#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.0' } +#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' } diff --git a/src/functions/public/Config/Set-GitHubConfig.ps1 b/src/functions/public/Config/Set-GitHubConfig.ps1 index 029b5640b..e70f260ce 100644 --- a/src/functions/public/Config/Set-GitHubConfig.ps1 +++ b/src/functions/public/Config/Set-GitHubConfig.ps1 @@ -54,4 +54,4 @@ Write-Debug "[$stackPath] - End" } } -#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.0' } +#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' } diff --git a/src/functions/public/Webhooks/Test-GitHubWebhookSignature.ps1 b/src/functions/public/Webhooks/Test-GitHubWebhookSignature.ps1 index 71b040663..a7e53b49a 100644 --- a/src/functions/public/Webhooks/Test-GitHubWebhookSignature.ps1 +++ b/src/functions/public/Webhooks/Test-GitHubWebhookSignature.ps1 @@ -5,9 +5,9 @@ .DESCRIPTION This function validates the integrity and authenticity of a GitHub webhook request by comparing - the received HMAC SHA-256 signature against a computed hash of the payload using a shared secret. - It uses a constant-time comparison to mitigate timing attacks and returns a boolean indicating - whether the signature is valid. + the received HMAC signature against a computed hash of the payload using a shared secret. + It uses the SHA-256 algorithm and employs a constant-time comparison to mitigate + timing attacks. The function returns a boolean indicating whether the signature is valid. .EXAMPLE Test-GitHubWebhookSignature -Secret $env:WEBHOOK_SECRET -Body $Request.RawBody -Signature $Request.Headers['X-Hub-Signature-256'] @@ -19,6 +19,16 @@ Validates the provided webhook payload against the HMAC SHA-256 signature using the given secret. + .EXAMPLE + Test-GitHubWebhookSignature -Secret $env:WEBHOOK_SECRET -Request $Request + + Output: + ```powershell + True + ``` + + Validates the webhook request using the entire request object, automatically extracting the body and signature. + .OUTPUTS bool @@ -29,11 +39,12 @@ .LINK https://psmodule.io/GitHub/Functions/Webhooks/Test-GitHubWebhookSignature - .LINK - https://docs.github.com/webhooks/using-webhooks/validating-webhook-deliveries + .NOTES + [Validating Webhook Deliveries | GitHub Docs](https://docs.github.com/webhooks/using-webhooks/validating-webhook-deliveries) + [Webhook events and payloads | GitHub Docs](https://docs.github.com/en/webhooks/webhook-events-and-payloads) #> [OutputType([bool])] - [CmdletBinding()] + [CmdletBinding(DefaultParameterSetName = 'ByBody')] param ( # The secret key used to compute the HMAC hash. # Example: 'mysecret' @@ -43,25 +54,59 @@ # The JSON body of the GitHub webhook request. # This must be the compressed JSON payload received from GitHub. # Example: '{"action":"opened"}' - [Parameter(Mandatory)] + [Parameter(Mandatory, ParameterSetName = 'ByBody')] [string] $Body, # The signature received from GitHub to compare against. # Example: 'sha256=abc123...' - [Parameter(Mandatory)] - [string] $Signature + [Parameter(Mandatory, ParameterSetName = 'ByBody')] + [string] $Signature, + + # The entire request object containing RawBody and Headers. + # Used in Azure Function Apps or similar environments. + [Parameter(Mandatory, ParameterSetName = 'ByRequest')] + [PSObject] $Request ) - $keyBytes = [Text.Encoding]::UTF8.GetBytes($Secret) - $payloadBytes = [Text.Encoding]::UTF8.GetBytes($Body) + begin { + $stackPath = Get-PSCallStackPath + Write-Debug "[$stackPath] - Start" + } - $hmac = [System.Security.Cryptography.HMACSHA256]::new() - $hmac.Key = $keyBytes - $hashBytes = $hmac.ComputeHash($payloadBytes) - $computedSignature = 'sha256=' + (($hashBytes | ForEach-Object { $_.ToString('x2') }) -join '') + process { + # Handle parameter sets + if ($PSCmdlet.ParameterSetName -eq 'ByRequest') { + $Body = $Request.RawBody + $Signature = $Request.Headers['X-Hub-Signature-256'] - [System.Security.Cryptography.CryptographicOperations]::FixedTimeEquals( - [Text.Encoding]::UTF8.GetBytes($computedSignature), - [Text.Encoding]::UTF8.GetBytes($Signature) - ) + # If signature not found, throw an error + if (-not $Signature) { + throw "No webhook signature found in request headers. Expected 'X-Hub-Signature-256' for SHA256 algorithm." + } + } + + $keyBytes = [Text.Encoding]::UTF8.GetBytes($Secret) + $payloadBytes = [Text.Encoding]::UTF8.GetBytes($Body) + + # Create HMAC SHA256 object + $hmac = [System.Security.Cryptography.HMACSHA256]::new() + $algorithmPrefix = 'sha256=' + + $hmac.Key = $keyBytes + $hashBytes = $hmac.ComputeHash($payloadBytes) + $computedSignature = $algorithmPrefix + (($hashBytes | ForEach-Object { $_.ToString('x2') }) -join '') + + # Dispose of the HMAC object + $hmac.Dispose() + + [System.Security.Cryptography.CryptographicOperations]::FixedTimeEquals( + [Text.Encoding]::UTF8.GetBytes($computedSignature), + [Text.Encoding]::UTF8.GetBytes($Signature) + ) + } + + end { + Write-Debug "[$stackPath] - End" + } } + diff --git a/tests/GitHub.Tests.ps1 b/tests/GitHub.Tests.ps1 index 474160fbf..988d7a891 100644 --- a/tests/GitHub.Tests.ps1 +++ b/tests/GitHub.Tests.ps1 @@ -179,6 +179,37 @@ Describe 'Auth' { } } +Describe 'Anonymous - Functions that can run anonymously' { + It 'Get-GithubRateLimit - Using -Anonymous' { + $rateLimit = Get-GitHubRateLimit -Anonymous + LogGroup 'Rate Limit' { + Write-Host ($rateLimit | Format-Table | Out-String) + } + $rateLimit | Should -Not -BeNullOrEmpty + } + It 'Invoke-GitHubAPI - Using -Anonymous' { + $rateLimit = Invoke-GitHubAPI -ApiEndpoint '/rate_limit' -Anonymous | Select-Object -ExpandProperty Response + LogGroup 'Rate Limit' { + Write-Host ($rateLimit | Format-Table | Out-String) + } + $rateLimit | Should -Not -BeNullOrEmpty + } + It 'Get-GithubRateLimit - Using -Context Anonymous' { + $rateLimit = Get-GitHubRateLimit -Context Anonymous + LogGroup 'Rate Limit' { + Write-Host ($rateLimit | Format-List | Out-String) + } + $rateLimit | Should -Not -BeNullOrEmpty + } + It 'Invoke-GitHubAPI - Using -Context Anonymous' { + $rateLimit = Invoke-GitHubAPI -ApiEndpoint '/rate_limit' -Context Anonymous | Select-Object -ExpandProperty Response + LogGroup 'Rate Limit' { + Write-Host ($rateLimit | Format-Table | Out-String) + } + $rateLimit | Should -Not -BeNullOrEmpty + } +} + Describe 'GitHub' { Context 'Config' { It 'Get-GitHubConfig - Gets the module configuration' { @@ -780,42 +811,40 @@ Describe 'Emojis' { } Describe 'Webhooks' { - It 'Test-GitHubWebhookSignature - Validates the webhook payload using known correct signature' { + BeforeAll { $secret = "It's a Secret to Everybody" $payload = 'Hello, World!' $signature = 'sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17' + } + + It 'Test-GitHubWebhookSignature - Validates the webhook payload using known correct signature (SHA256)' { $result = Test-GitHubWebhookSignature -Secret $secret -Body $payload -Signature $signature $result | Should -Be $true } -} -Describe 'Anonymous - Functions that can run anonymously' { - It 'Get-GithubRateLimit - Using -Anonymous' { - $rateLimit = Get-GitHubRateLimit -Anonymous - LogGroup 'Rate Limit' { - Write-Host ($rateLimit | Format-Table | Out-String) - } - $rateLimit | Should -Not -BeNullOrEmpty - } - It 'Invoke-GitHubAPI - Using -Anonymous' { - $rateLimit = Invoke-GitHubAPI -ApiEndpoint '/rate_limit' -Anonymous | Select-Object -ExpandProperty Response - LogGroup 'Rate Limit' { - Write-Host ($rateLimit | Format-Table | Out-String) + It 'Test-GitHubWebhookSignature - Validates the webhook using Request object' { + $mockRequest = [PSCustomObject]@{ + RawBody = $payload + Headers = @{ + 'X-Hub-Signature-256' = $signature + } } - $rateLimit | Should -Not -BeNullOrEmpty + $result = Test-GitHubWebhookSignature -Secret $secret -Request $mockRequest + $result | Should -Be $true } - It 'Get-GithubRateLimit - Using -Context Anonymous' { - $rateLimit = Get-GitHubRateLimit -Context Anonymous - LogGroup 'Rate Limit' { - Write-Host ($rateLimit | Format-List | Out-String) - } - $rateLimit | Should -Not -BeNullOrEmpty + + It 'Test-GitHubWebhookSignature - Should fail with invalid signature' { + $invalidSignature = 'sha256=invalid' + $result = Test-GitHubWebhookSignature -Secret $secret -Body $payload -Signature $invalidSignature + $result | Should -Be $false } - It 'Invoke-GitHubAPI - Using -Context Anonymous' { - $rateLimit = Invoke-GitHubAPI -ApiEndpoint '/rate_limit' -Context Anonymous | Select-Object -ExpandProperty Response - LogGroup 'Rate Limit' { - Write-Host ($rateLimit | Format-Table | Out-String) + + It 'Test-GitHubWebhookSignature - Should throw when signature header is missing from request' { + $mockRequest = [PSCustomObject]@{ + RawBody = $payload + Headers = @{} } - $rateLimit | Should -Not -BeNullOrEmpty + + { Test-GitHubWebhookSignature -Secret $secret -Request $mockRequest } | Should -Throw } } From 2dff5b76cd2a89d4f35c6582fbeb9a8ea2854f47 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 6 Aug 2025 12:33:35 +0200 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=AA=B2=20[Fix]:=20Skip=20revoke=20tok?= =?UTF-8?q?en=20if=20token=20is=20expired=20(#488)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This pull request refactors the conditional logic in the `Disconnect-GitHubAccount` function to improve readability and debugging and that actually skips the revocation of the token if it is expired. Refactoring and debugging improvements: * Added a condition to check if the token is expired before running the `Revoke-GitHubAppInstallationAccessToken`. The issue here was that the function would fail if it was expired. * Split the previous compound conditional into three distinct variables: `$isNotGitHubToken`, `$isIATAuthType`, and `$isNotExpired` for clarity and maintainability. * Added `Write-Debug` statements for each condition to facilitate easier troubleshooting and understanding of the script's flow. ## Type of change - [ ] 📖 [Docs] - [x] 🪲 [Fix] - [ ] 🩹 [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 --- src/functions/public/Auth/Disconnect-GitHubAccount.ps1 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/functions/public/Auth/Disconnect-GitHubAccount.ps1 b/src/functions/public/Auth/Disconnect-GitHubAccount.ps1 index aa27959e0..0007e1958 100644 --- a/src/functions/public/Auth/Disconnect-GitHubAccount.ps1 +++ b/src/functions/public/Auth/Disconnect-GitHubAccount.ps1 @@ -59,8 +59,13 @@ $contextItem = Resolve-GitHubContext -Context $contextItem $contextToken = Get-GitHubAccessToken -Context $contextItem -AsPlainText - $isGitHubToken = $contextToken -eq (Get-GitHubToken | ConvertFrom-SecureString -AsPlainText) - if (-not $isGitHubToken -and $contextItem.AuthType -eq 'IAT') { + $isNotGitHubToken = -not ($contextToken -eq (Get-GitHubToken | ConvertFrom-SecureString -AsPlainText)) + $isIATAuthType = $contextItem.AuthType -eq 'IAT' + $isNotExpired = $contextItem.TokenExpiresIn -gt 0 + Write-Debug "isNotGitHubToken: $isNotGitHubToken" + Write-Debug "isIATAuthType: $isIATAuthType" + Write-Debug "isNotExpired: $isNotExpired" + if ($isNotGitHubToken -and $isIATAuthType -and $isNotExpired) { Revoke-GitHubAppInstallationAccessToken -Context $contextItem } From 036e74a72f583d4a135558175f73a9d659002baa Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 6 Aug 2025 23:28:12 +0200 Subject: [PATCH 5/8] =?UTF-8?q?=F0=9F=A9=B9=20[Patch]:=20Update=20the=20ex?= =?UTF-8?q?amples=20for=20how=20to=20connect=20using=20the=20module=20(#48?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This pull request updates the `examples/Connecting.ps1` script to clarify and expand the usage examples for GitHub connection methods, including new authentication flows and improved documentation. The changes also make some minor corrections to comments and example commands related to context management. Authentication and connection improvements: * Added examples for connecting to GitHub programmatically using a token and clarified support for both fine-grained and classic PATs in the `Connect-GitHub` command. * Added new examples for connecting using a GitHub App with a private key stored in Azure Key Vault, including the use of `Connect-GitHubApp` for organizational contexts. * Improved comments for OAuth App, Device Flow, and PAT authentication flows to clarify best practices and supported scenarios. Context and profile management: * Updated comments to clarify that context names can be tab-completed and corrected a typo in the `Switch-GitHubContext` example (`Switch-GitHubCwontext`). ## 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 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- examples/Connecting.ps1 | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/examples/Connecting.ps1 b/examples/Connecting.ps1 index 810b6d05a..005786ce0 100644 --- a/examples/Connecting.ps1 +++ b/examples/Connecting.ps1 @@ -7,22 +7,30 @@ Connect-GitHub # Log on to a specific instance of GitHub (enterprise) Connect-GitHub -Host 'msx.ghe.com' -Get-GitHubRepository -Context 'msx.ghe.com/MariusStorhaug' # Contexts should be selectable/overrideable on any call +Get-GitHubRepository -Context 'msx.ghe.com/MariusStorhaug' # Contexts are selectable/overrideable on any call -# Connect to GitHub interactively using OAuth App and Device Flow (should not use this, should we even support it?) +# Connect to GitHub interactively using OAuth App and Device Flow. Connect-GitHub -Mode 'OAuthApp' -Scope 'gist read:org repo workflow' -# Connect to GitHub interactively using less desired PAT flow +# Connect to GitHub interactively using less desired PAT flow, supports both fine-grained and classic PATs Connect-GitHub -UseAccessToken +# Connect to GitHub programatically (GitHub App Installation Access Token or PAT) +Connect-GitHub -Token *********** + # Connect to GitHub programatically (GitHub Actions) Connect-GitHub # Looks for the GITHUB_TOKEN variable -# Connect to GitHub programatically (GitHub App, for GitHub Actions or external applications, JWT login) +# Connect using a GitHub App and its private key (local signing of JWT) Connect-GitHub -ClientID '' -PrivateKey '' -# Connect to GitHub programatically (GitHub App Installation Access Token or PAT) -Connect-GitHub -Token *********** +# Connect using a GitHub App and the Key vault for signing the JWT. +# Prereq: The private key is stored in an Azure Key Vault and the shell has an authenticated Azure PowerShell or Azure CLI session +$ClientID = 'Iv23lieHcDQDwVV3alK1' +$KeyVaultKeyReference = 'https://psmodule-test-vault.vault.azure.net/keys/psmodule-ent-app' +Connect-GitHub -ClientID $ClientID -KeyVaultKeyReference $KeyVaultKeyReference +Connect-GitHubApp -Organization 'dnb-tooling' + ### ### Contexts / Profiles @@ -37,14 +45,12 @@ Get-GitHubContext -ListAvailable # Returns a specific context, autocomplete the name. Get-GitHubContext -Context 'msx.ghe.com/MariusStorhaug' -# Take a name dynamically from Get-GitHubContext? Autocomplete the name +# Take a name dynamically from Get-GitHubContext? tab-complete the name Switch-GitHubContext -Context 'msx.ghe.com/MariusStorhaug' # Set a specific context as the default context using pipeline 'msx.ghe.com/MariusStorhaug' | Switch-GitHubContext -Get-GitHubContext -Context 'github.com/MariusStorhaug' | Switch-GitHubContext - # Abstraction layers on GitHubContexts Get-GitHubContext -Context 'msx.ghe.com/MariusStorhaug' From 9550a2986a53e89152dda80ffff89a67c40317fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 08:29:49 +0200 Subject: [PATCH 6/8] Bump actions/checkout from 4 to 5 (#490) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
Release notes

Sourced from actions/checkout's releases.

v5.0.0

What's Changed

⚠️ Minimum Compatible Runner Version

v2.327.1
Release Notes

Make sure your runner is updated to this version or newer to use this release.

Full Changelog: https://github.com/actions/checkout/compare/v4...v5.0.0

v4.3.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v4...v4.3.0

v4.2.2

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v4.2.1...v4.2.2

v4.2.1

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v4.2.0...v4.2.1

... (truncated)

Changelog

Sourced from actions/checkout's changelog.

Changelog

V5.0.0

V4.3.0

v4.2.2

v4.2.1

v4.2.0

v4.1.7

v4.1.6

v4.1.5

v4.1.4

v4.1.3

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/Linter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Linter.yml b/.github/workflows/Linter.yml index 6c163eb41..8017f6334 100644 --- a/.github/workflows/Linter.yml +++ b/.github/workflows/Linter.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 From 05728fd3b4eab9da99de3e0308cc540c88c67715 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Sep 2025 14:33:36 +0200 Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=A9=B9=20Add=20support=20for=20Linux?= =?UTF-8?q?=20ARM64=20(#492)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request updates the required module versions for the `Context` and `Sodium` modules across both private and public function. Module version updates: * Updated the required version of the `Context` module from `8.1.1` to `8.1.3` in all relevant scripts, including `Remove-GitHubContext.ps1`, `Set-GitHubContext.ps1`, `Initialize-GitHubConfig.ps1`, `Get-GitHubContext.ps1`, `Get-GitHubConfig.ps1`, `Remove-GitHubConfig.ps1`, and `Set-GitHubConfig.ps1`. * Updated the required version of the `Sodium` module from `2.2.0` to `2.2.2` in `Set-GitHubSecret.ps1`. --------- 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 --- .../Auth/Context/Remove-GitHubContext.ps1 | 2 +- .../private/Auth/Context/Set-GitHubContext.ps1 | 3 ++- .../private/Config/Initialize-GitHubConfig.ps1 | 3 ++- .../public/Auth/Context/Get-GitHubContext.ps1 | 3 ++- .../public/Config/Get-GitHubConfig.ps1 | 3 ++- .../public/Config/Remove-GitHubConfig.ps1 | 3 ++- .../public/Config/Set-GitHubConfig.ps1 | 3 ++- .../public/Secrets/Set-GitHubSecret.ps1 | 2 +- tests/Secrets.Tests.ps1 | 18 +++++++----------- 9 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/functions/private/Auth/Context/Remove-GitHubContext.ps1 b/src/functions/private/Auth/Context/Remove-GitHubContext.ps1 index e74e8aa5d..6186b4807 100644 --- a/src/functions/private/Auth/Context/Remove-GitHubContext.ps1 +++ b/src/functions/private/Auth/Context/Remove-GitHubContext.ps1 @@ -42,4 +42,4 @@ Write-Debug "[$stackPath] - End" } } -#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' } +#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.3' } diff --git a/src/functions/private/Auth/Context/Set-GitHubContext.ps1 b/src/functions/private/Auth/Context/Set-GitHubContext.ps1 index b38085248..c20ea4281 100644 --- a/src/functions/private/Auth/Context/Set-GitHubContext.ps1 +++ b/src/functions/private/Auth/Context/Set-GitHubContext.ps1 @@ -168,4 +168,5 @@ Write-Debug "[$stackPath] - End" } } -#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' } +#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.3' } + diff --git a/src/functions/private/Config/Initialize-GitHubConfig.ps1 b/src/functions/private/Config/Initialize-GitHubConfig.ps1 index 3ca1bc34d..651445856 100644 --- a/src/functions/private/Config/Initialize-GitHubConfig.ps1 +++ b/src/functions/private/Config/Initialize-GitHubConfig.ps1 @@ -75,4 +75,5 @@ Write-Debug "[$stackPath] - End" } } -#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' } +#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.3' } + diff --git a/src/functions/public/Auth/Context/Get-GitHubContext.ps1 b/src/functions/public/Auth/Context/Get-GitHubContext.ps1 index ccdd620ae..9f0015551 100644 --- a/src/functions/public/Auth/Context/Get-GitHubContext.ps1 +++ b/src/functions/public/Auth/Context/Get-GitHubContext.ps1 @@ -92,4 +92,5 @@ Write-Debug "[$stackPath] - End" } } -#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' } +#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.3' } + diff --git a/src/functions/public/Config/Get-GitHubConfig.ps1 b/src/functions/public/Config/Get-GitHubConfig.ps1 index e8a0ee1bc..9471de437 100644 --- a/src/functions/public/Config/Get-GitHubConfig.ps1 +++ b/src/functions/public/Config/Get-GitHubConfig.ps1 @@ -47,4 +47,5 @@ Write-Debug "[$stackPath] - End" } } -#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' } +#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.3' } + diff --git a/src/functions/public/Config/Remove-GitHubConfig.ps1 b/src/functions/public/Config/Remove-GitHubConfig.ps1 index e432b7044..8ae72d979 100644 --- a/src/functions/public/Config/Remove-GitHubConfig.ps1 +++ b/src/functions/public/Config/Remove-GitHubConfig.ps1 @@ -44,4 +44,5 @@ Write-Debug "[$stackPath] - End" } } -#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' } +#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.3' } + diff --git a/src/functions/public/Config/Set-GitHubConfig.ps1 b/src/functions/public/Config/Set-GitHubConfig.ps1 index e70f260ce..648ab97a7 100644 --- a/src/functions/public/Config/Set-GitHubConfig.ps1 +++ b/src/functions/public/Config/Set-GitHubConfig.ps1 @@ -54,4 +54,5 @@ Write-Debug "[$stackPath] - End" } } -#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' } +#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.3' } + diff --git a/src/functions/public/Secrets/Set-GitHubSecret.ps1 b/src/functions/public/Secrets/Set-GitHubSecret.ps1 index a92474e0e..c1da2a007 100644 --- a/src/functions/public/Secrets/Set-GitHubSecret.ps1 +++ b/src/functions/public/Secrets/Set-GitHubSecret.ps1 @@ -143,4 +143,4 @@ Write-Debug "[$stackPath] - End" } } -#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.2.0'} +#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.2.2'} diff --git a/tests/Secrets.Tests.ps1 b/tests/Secrets.Tests.ps1 index 2384187ab..48bff8af7 100644 --- a/tests/Secrets.Tests.ps1 +++ b/tests/Secrets.Tests.ps1 @@ -129,6 +129,7 @@ Describe 'Secrets' { Write-Host ($org | Format-List | Out-String) } } + Context 'PublicKey' { It 'Get-GitHubPublicKey - Action' { $result = Get-GitHubPublicKey @scope @@ -139,18 +140,13 @@ Describe 'Secrets' { } It 'Get-GitHubPublicKey - Codespaces' { - switch ($org.plan.name) { - 'free' { - { Get-GitHubPublicKey @scope -Type codespaces } | Should -Throw - } - default { - $result = Get-GitHubPublicKey @scope -Type codespaces - LogGroup 'PublicKey' { - Write-Host "$($result | Select-Object * | Format-Table -AutoSize | Out-String)" - } - $result | Should -Not -BeNullOrEmpty - } + $plan = $org.plan.name + Write-Host "Running with plan [$plan]" + $result = Get-GitHubPublicKey @scope -Type codespaces + LogGroup 'PublicKey' { + Write-Host "$($result | Select-Object * | Format-Table -AutoSize | Out-String)" } + $result | Should -Not -BeNullOrEmpty } } From 82939b5cb2146f7eba6c546e421505722a75ae86 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 4 Sep 2025 12:39:31 +0200 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=A9=B9=20[Patch]:=20Add=20`TokenExpir?= =?UTF-8?q?esAt`=20property=20and=20update=20expiration=20logic=20for=20Gi?= =?UTF-8?q?tHubAppContext=20(#494)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This pull request improves how token expiry information is handled and displayed for GitHub authentication contexts in both code and documentation. The main changes introduce automatic token renewal for GitHub Apps, add new properties to track token expiry, update formatting and type definitions to support these properties, and enhance tests to validate the new behavior. **Authentication and token management improvements:** * Added documentation in `README.md` explaining that short-lived tokens (for GitHub Apps) are automatically renewed by the module, clarifying the difference from long-lived tokens. * Removed outdated/duplicated token renewal documentation and examples from `README.md` to streamline the explanation. **Code and formatting updates:** * Added `TokenExpiresAt` property to `GitHubContext` and `GitHubAppContext` classes, and implemented a `TokenExpiresIn` script property for `GitHubAppContext` to calculate remaining token time. * Updated `GitHubContext.Format.ps1xml` to display `TokenExpiresAt` and `TokenExpiresIn` in relevant views, adjusted expiry logic (`-le 0` instead of `-lt 0`), and added special handling for token types (e.g., 10-minute expiry for APP tokens). **Testing enhancements:** * Extended tests in `GitHub.Tests.ps1` to verify that authentication contexts include valid `TokenExpiresAt` and `TokenExpiresIn` properties. ## 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 --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> --- README.md | 36 +++------- src/classes/public/Config/GitHubConfig.ps1 | 3 - src/classes/public/Context/GitHubContext.ps1 | 1 + src/formats/GitHubConfig.Format.ps1xml | 3 - src/formats/GitHubContext.Format.ps1xml | 35 ++++++---- .../GitHub Apps/New-GitHubUnsignedJWT.ps1 | 4 +- .../Test-GitHubJWTRefreshRequired.ps1 | 2 +- src/types/GitHubContext.Types.ps1xml | 20 +++++- src/variables/private/GitHub.ps1 | 1 - tests/Emojis.Tests.ps1 | 67 +++++++++++++++++++ tests/GitHub.Tests.ps1 | 52 ++------------ 11 files changed, 125 insertions(+), 99 deletions(-) create mode 100644 tests/Emojis.Tests.ps1 diff --git a/README.md b/README.md index 583424976..c84fbb0f4 100644 --- a/README.md +++ b/README.md @@ -70,34 +70,7 @@ Press Enter to open github.com in your browser...: #-> Press enter and paste th After this you will need to install the GitHub App on the repos you want to manage. You can do this by visiting the [PowerShell for GitHub](https://github.com/apps/powershell-for-github) app page. -> Info: We will be looking to include this as a check in the module in the future. So it becomes a part of the regular sign in process. -Consecutive runs of the `Connect-GitHubAccount` will not require you to paste the code again unless you revoke the token -or you change the type of authentication you want to use. Instead, it checks the remaining duration of the access token and -uses the refresh token to get a new access token if its less than 4 hours remaining. - -```powershell -Connect-GitHubAccount -✓ Access token is still valid for 05:30:41 ... -✓ Logged in as octocat! -``` - -This is also happening automatically when you run a command that requires authentication. The validity of the token is checked before the command is executed. -If it is no longer valid, the token is refreshed and the command is executed. - -```powershell -Connect-GitHubAccount -⚠ Access token remaining validity 01:22:31. Refreshing access token... -✓ Logged in as octocat! -``` - -If the timer has gone out, we still have your back. It will just refresh as long as the refresh token is valid. - -```powershell -Connect-GitHubAccount -⚠ Access token expired. Refreshing access token... -✓ Logged in as octocat! -``` #### Personal authentication - User access tokens with OAuth app @@ -223,6 +196,15 @@ Connect-GitHubAccount -Host 'https://msx.ghe.com' -ClientID 'lv123456789' ✓ Logged in as octocat! ``` +#### Automatic token renewal + +The module automatically manages short‑lived tokens for GitHub Apps: + +- User access tokens (when you authenticate via a GitHub App) are short‑lived and include a refresh token. The module refreshes them automatically before/when they expire—no extra steps are required. +- App JWTs (when the context is a GitHub App) are generated and rotated automatically per call as needed. You never need to create or renew the JWT yourself. + +Note: Long‑lived tokens like classic/fine‑grained PATs and provided installation tokens (GH_TOKEN/GITHUB_TOKEN) are not refreshed by the module. + ### Command Exploration Familiarize yourself with the available cmdlets using the module's comprehensive documentation or inline help. diff --git a/src/classes/public/Config/GitHubConfig.ps1 b/src/classes/public/Config/GitHubConfig.ps1 index ce745b2de..ac8ffd8b8 100644 --- a/src/classes/public/Config/GitHubConfig.ps1 +++ b/src/classes/public/Config/GitHubConfig.ps1 @@ -35,9 +35,6 @@ # The default value for retry interval in seconds. [System.Nullable[int]] $RetryInterval - # The tolerance time in seconds for JWT token validation. - [System.Nullable[int]] $JwtTimeTolerance - # The environment type, which is used to determine the context of the GitHub API calls. [string] $EnvironmentType diff --git a/src/classes/public/Context/GitHubContext.ps1 b/src/classes/public/Context/GitHubContext.ps1 index 8788d051b..7d437c6de 100644 --- a/src/classes/public/Context/GitHubContext.ps1 +++ b/src/classes/public/Context/GitHubContext.ps1 @@ -82,6 +82,7 @@ $this.UserName = $Object.UserName $this.Token = $Object.Token $this.TokenType = $Object.TokenType + $this.TokenExpiresAt = $Object.TokenExpiresAt $this.Enterprise = $Object.Enterprise $this.Owner = $Object.Owner $this.Repository = $Object.Repository diff --git a/src/formats/GitHubConfig.Format.ps1xml b/src/formats/GitHubConfig.Format.ps1xml index 51fb2c86b..cd556ceb4 100644 --- a/src/formats/GitHubConfig.Format.ps1xml +++ b/src/formats/GitHubConfig.Format.ps1xml @@ -31,9 +31,6 @@ AccessTokenGracePeriodInHours - - JwtTimeTolerance - ApiVersion diff --git a/src/formats/GitHubContext.Format.ps1xml b/src/formats/GitHubContext.Format.ps1xml index 90ae28648..45756fbb5 100644 --- a/src/formats/GitHubContext.Format.ps1xml +++ b/src/formats/GitHubContext.Format.ps1xml @@ -58,7 +58,7 @@ return } - if ($_.TokenExpiresIn -lt 0) { + if ($_.TokenExpiresIn -le 0) { $text = 'Expired' } else { $text = $_.TokenExpiresIn.ToString('hh\:mm\:ss') @@ -73,6 +73,9 @@ 'IAT' { $maxValue = [TimeSpan]::FromHours(1) } + 'APP' { + $maxValue = [TimeSpan]::FromMinutes(10) + } } $ratio = [Math]::Min(($_.TokenExpiresIn / $maxValue), 1) [GitHubFormatter]::FormatColorByRatio($ratio, $text) @@ -172,6 +175,12 @@ TokenType + + TokenExpiresAt + + + TokenExpiresIn + HostName @@ -238,22 +247,22 @@ TokenType - HostName + TokenExpiresAt - UserName + TokenExpiresIn - ClientID + HostName - InstallationID + UserName - TokenExpiresAt + ClientID - TokenExpiresIn + InstallationID InstallationType @@ -311,6 +320,12 @@ TokenType + + TokenExpiresAt + + + TokenExpiresIn + HostName @@ -326,12 +341,6 @@ Scope - - TokenExpiresAt - - - TokenExpiresIn - RefreshTokenExpiresAt diff --git a/src/functions/private/Apps/GitHub Apps/New-GitHubUnsignedJWT.ps1 b/src/functions/private/Apps/GitHub Apps/New-GitHubUnsignedJWT.ps1 index 2d47a9f81..b6aef3d64 100644 --- a/src/functions/private/Apps/GitHub Apps/New-GitHubUnsignedJWT.ps1 +++ b/src/functions/private/Apps/GitHub Apps/New-GitHubUnsignedJWT.ps1 @@ -45,8 +45,8 @@ } ) $now = [System.DateTimeOffset]::UtcNow - $iat = $now.AddSeconds(-$script:GitHub.Config.JwtTimeTolerance) - $exp = $now.AddSeconds($script:GitHub.Config.JwtTimeTolerance) + $iat = $now.AddMinutes(-10) + $exp = $now.AddMinutes(10) $payload = [GitHubJWTComponent]::ToBase64UrlString( @{ iat = $iat.ToUnixTimeSeconds() diff --git a/src/functions/private/Apps/GitHub Apps/Test-GitHubJWTRefreshRequired.ps1 b/src/functions/private/Apps/GitHub Apps/Test-GitHubJWTRefreshRequired.ps1 index ae705ead3..4db614e1f 100644 --- a/src/functions/private/Apps/GitHub Apps/Test-GitHubJWTRefreshRequired.ps1 +++ b/src/functions/private/Apps/GitHub Apps/Test-GitHubJWTRefreshRequired.ps1 @@ -31,7 +31,7 @@ function Test-GitHubJWTRefreshRequired { process { try { - ($Context.TokenExpiresAt - [datetime]::Now).TotalSeconds -le ($script:GitHub.Config.JwtTimeTolerance / 2) + ($Context.TokenExpiresAt - [datetime]::Now).TotalSeconds -le 60 } catch { return $true } diff --git a/src/types/GitHubContext.Types.ps1xml b/src/types/GitHubContext.Types.ps1xml index 2e78158cb..d6cb55a8c 100644 --- a/src/types/GitHubContext.Types.ps1xml +++ b/src/types/GitHubContext.Types.ps1xml @@ -6,7 +6,7 @@ TokenExpiresIn - if ($null -eq $this.TokenExpiresAt) { return } + if ($null -eq $this.TokenExpiresAt) { return [TimeSpan]::Zero } $timeRemaining = $this.TokenExpiresAt - [DateTime]::Now if ($timeRemaining.TotalSeconds -lt 0) { return [TimeSpan]::Zero @@ -17,7 +17,7 @@ RefreshTokenExpiresIn - if ($null -eq $this.RefreshTokenExpiresAt) { return } + if ($null -eq $this.RefreshTokenExpiresAt) { return [TimeSpan]::Zero } $timeRemaining = $this.RefreshTokenExpiresAt - [DateTime]::Now if ($timeRemaining.TotalSeconds -lt 0) { return [TimeSpan]::Zero @@ -29,6 +29,22 @@
GitHubAppInstallationContext + + + TokenExpiresIn + + if ($null -eq $this.TokenExpiresAt) { return [TimeSpan]::Zero } + $timeRemaining = $this.TokenExpiresAt - [DateTime]::Now + if ($timeRemaining.TotalSeconds -lt 0) { + return [TimeSpan]::Zero + } + return $timeRemaining + + + + + + GitHubAppContext TokenExpiresIn diff --git a/src/variables/private/GitHub.ps1 b/src/variables/private/GitHub.ps1 index 93d1d1344..3891118b3 100644 --- a/src/variables/private/GitHub.ps1 +++ b/src/variables/private/GitHub.ps1 @@ -18,7 +18,6 @@ $script:GitHub = [pscustomobject]@{ PerPage = 100 RetryCount = 0 RetryInterval = 1 - JwtTimeTolerance = 300 EnvironmentType = Get-GitHubEnvironmentType } Config = $null diff --git a/tests/Emojis.Tests.ps1 b/tests/Emojis.Tests.ps1 new file mode 100644 index 000000000..ff3100a2e --- /dev/null +++ b/tests/Emojis.Tests.ps1 @@ -0,0 +1,67 @@ +#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 'Emojis' { + $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) + } + + # Tests for APP goes here + if ($AuthType -eq 'APP') { + It 'Connect-GitHubApp - Connects as a GitHub App to ' { + $context = Connect-GitHubApp @connectAppParams -PassThru -Default -Silent + LogGroup 'Context' { + Write-Host ($context | Format-List | Out-String) + } + } + } + + # Tests for runners goes here + if ($Type -eq 'GitHub Actions') {} + + # Tests for IAT UAT and PAT goes here + It 'Get-GitHubEmoji - Gets a list of all emojis' { + $emojis = Get-GitHubEmoji + LogGroup 'emojis' { + Write-Host ($emojis | Format-Table | Out-String) + } + $emojis | Should -Not -BeNullOrEmpty + } + It 'Get-GitHubEmoji - Downloads all emojis' { + Get-GitHubEmoji -Path $Home + LogGroup 'emojis' { + $emojis = Get-ChildItem -Path $Home -File + Write-Host ($emojis | Format-Table | Out-String) + } + $emojis | Should -Not -BeNullOrEmpty + } + } +} diff --git a/tests/GitHub.Tests.ps1 b/tests/GitHub.Tests.ps1 index 988d7a891..53b9e347e 100644 --- a/tests/GitHub.Tests.ps1 +++ b/tests/GitHub.Tests.ps1 @@ -79,6 +79,9 @@ Describe 'Auth' { Write-Host ($context | Format-List | Out-String) } $context | Should -Not -BeNullOrEmpty + $context | Should -BeOfType [GitHubContext] + $context.TokenExpiresAt | Should -BeOfType [DateTime] + $context.TokenExpiresIn | Should -BeOfType [TimeSpan] } It 'Connect-GitHubApp - Connects as a GitHub App to ' -Skip:($AuthType -ne 'APP') { @@ -106,6 +109,8 @@ Describe 'Auth' { LogGroup 'Connect-GithubApp' { $context } + $context.TokenExpiresAt | Should -BeOfType [DateTime] + $context.TokenExpiresIn | Should -BeOfType [TimeSpan] LogGroup 'Context' { Write-Host ($context | Format-List | Out-String) } @@ -763,53 +768,6 @@ Describe 'API' { } } -Describe 'Emojis' { - $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) - } - - # Tests for APP goes here - if ($AuthType -eq 'APP') { - It 'Connect-GitHubApp - Connects as a GitHub App to ' { - $context = Connect-GitHubApp @connectAppParams -PassThru -Default -Silent - LogGroup 'Context' { - Write-Host ($context | Format-List | Out-String) - } - } - } - - # Tests for runners goes here - if ($Type -eq 'GitHub Actions') {} - - # Tests for IAT UAT and PAT goes here - It 'Get-GitHubEmoji - Gets a list of all emojis' { - $emojis = Get-GitHubEmoji - LogGroup 'emojis' { - Write-Host ($emojis | Format-Table | Out-String) - } - $emojis | Should -Not -BeNullOrEmpty - } - It 'Get-GitHubEmoji - Downloads all emojis' { - Get-GitHubEmoji -Path $Home - LogGroup 'emojis' { - $emojis = Get-ChildItem -Path $Home -File - Write-Host ($emojis | Format-Table | Out-String) - } - $emojis | Should -Not -BeNullOrEmpty - } - } -} - Describe 'Webhooks' { BeforeAll { $secret = "It's a Secret to Everybody"