Skip to content

Commit 8427e91

Browse files
🩹 [Patch]: Add function to remove app installation as a GitHub App (#483)
## Description This pull request introduces improvements to the GitHub App uninstallation workflow, adding more flexible and robust ways to uninstall apps both as an authenticated app and as an enterprise installation. The changes include new internal functions, enhanced parameter handling, improved context validation, and expanded test coverage to ensure reliability and clarity in uninstall scenarios. **Enhancements to GitHub App Uninstallation:** * Added new function `Uninstall-GitHubAppAsApp` to support uninstalling app installations as the authenticated app, with support for confirmation and verbose output. * Updated `Uninstall-GitHubAppOnEnterpriseOrganization` to clarify its purpose, improve parameter validation, add confirmation support, and provide better feedback on successful uninstalls. **Expanded and Flexible Public API:** * Refactored `Uninstall-GitHubApp` to support multiple uninstallation modes (by target, by object, by installation ID, by app slug), improved parameter sets, and added context and authentication checks for safer operation. **Documentation and Examples:** * Added a comprehensive example script `examples/Apps/UninstallingApps.ps1` showing various uninstallation scenarios for both app and enterprise contexts. **Testing and Reliability:** * Improved organization tests to clean up existing app installations before and after tests, and added a test to verify app uninstall behavior after organization deletion. **Other Improvements:** * Increased robustness in token revocation error handling in `Disconnect-GitHubAccount`. * Fixed a minor property default in `GitHubContext.Types.ps1xml` for better handling of missing token expiration. * Updated `.github/PSModule.yml` to enable and skip specific test and build steps as appropriate. ## Type of change <!-- Use the check-boxes [x] on the options that are relevant. --> - [ ] 📖 [Docs] - [ ] 🪲 [Fix] - [x] 🩹 [Patch] - [ ] ⚠️ [Security fix] - [ ] 🚀 [Feature] - [ ] 🌟 [Breaking change] ## Checklist <!-- Use the check-boxes [x] on the options that are relevant. --> - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas
1 parent 4bf12ff commit 8427e91

File tree

7 files changed

+306
-42
lines changed

7 files changed

+306
-42
lines changed

examples/Apps/UninstallingApps.ps1

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
## The uninstall function is context aware and will use the context to determine the type of uninstallation.
2+
## We can uninstall either as a GitHub App or as an Enterprise installation.
3+
## Examples assume that the GitHub App that is performing the uninstalls is the current context.
4+
## See 'Connection' examples to find how to connect as a GitHub App.
5+
6+
7+
##
8+
## As a GitHub App you can uninstall the app from any target where it is currently installed.
9+
##
10+
11+
# First get info about the app installations for the authenticated app.
12+
$installations = Get-GitHubAppInstallation
13+
$installations
14+
15+
# Uninstall the app installation by name. This will string match with the target of the installation.
16+
Uninstall-GitHubApp -Target 'msx' # Enterprise
17+
Uninstall-GitHubApp -Target 'org-123' # Organization
18+
Uninstall-GitHubApp -Target 'octocat' # User
19+
20+
# Uninstall the app installation by ID. This will do an exact match with the installation ID.
21+
Uninstall-GitHubApp -Target 12345
22+
23+
# Uninstall the app using the installation objects from the pipeline.
24+
Get-GitHubAppInstallation | Uninstall-GitHubApp
25+
26+
# Uninstall the app from all Users using an installation object array.
27+
$installations = Get-GitHubAppInstallation | Where-Object Type -EQ 'User'
28+
$installations | Uninstall-GitHubApp
29+
30+
###
31+
### Full example, uninstalling an app from deleted organizations in an enterprise.
32+
###
33+
# Get the installations for all organizations where the app is installed.
34+
$orgInstallations = Get-GitHubAppInstallation | Where-Object Type -EQ 'Organization'
35+
36+
# Connect to the enterprise using a management app that can manage installations to get the available organizations in the enterprise.
37+
$enterpriseContext = Connect-GitHubApp -Enterprise 'msx' -PassThru
38+
$orgs = Get-GitHubOrganization -Enterprise 'msx' -Context $enterpriseContext
39+
40+
# Uninstall the app from all organizations that are not in the list of available organizations.
41+
$orgInstallations | Where-Object { $_.Target -notin $orgs.Name } | Uninstall-GitHubApp
42+
43+
##
44+
## As an enterprise installation, you can uninstall any app that is installed on an organization in the enterprise.
45+
##
46+
47+
# Get the installations for the organizations in the enterprise that we can manage.
48+
$orgInstallations = Get-GitHubAppInstallation | Where-Object Type -EQ 'Organization'
49+
50+
# Connect to the enterprise using a management app that can manage installations and store it in a variable.
51+
$enterpriseContext = Connect-GitHubApp -Enterprise 'msx' -PassThru
52+
53+
# Get the available organizations in the enterprise.
54+
$orgs = Get-GitHubOrganization -Enterprise 'msx' -Context $enterpriseContext
55+
56+
# Lets say we want to uninstall a specific app from all organizations in the enterprise.
57+
# We can do this by iterating over the installations that we manage and uninstall the app.
58+
$appToUninstall = 'psmodule-enterprise-app'
59+
foreach ($managedOrg in $orgInstallations) {
60+
Uninstall-GitHubApp -Target $managedOrg.Target -AppSlug $appToUninstall -Context $enterpriseContext
61+
}
62+
63+
# Uninstall an app installation by name.
64+
Uninstall-GitHubApp -Target 'msx' -AppName 'my-app'
65+
66+
$installations | Uninstall-GitHubApp -Target 'enterprise-name'
67+
68+
# Uninstall an app installation by object.
69+
$installations | Uninstall-GitHubApp
70+
71+
72+
# Uninstall an app installation by ID.
73+
Uninstall-GitHubApp -Target 12345
74+
75+
76+
Uninstall-GitHubApp -Target 'fnxsd' -ID 1234567890
77+
Uninstall-GitHubApp -Target 'fnxsd' -Slug 'my-app-slug'
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
function Uninstall-GitHubAppAsApp {
2+
<#
3+
.SYNOPSIS
4+
Delete an installation for the authenticated app.
5+
6+
.DESCRIPTION
7+
Deletes a GitHub App installation using the authenticated App context.
8+
9+
.EXAMPLE
10+
Uninstall-GitHubAppAsApp -ID 123456 -Context $appContext
11+
12+
Deletes the installation with ID 123456 for the authenticated app.
13+
14+
.NOTES
15+
[Delete an installation for the authenticated app](https://docs.github.com/rest/apps/installations#delete-an-installation-for-the-authenticated-app)
16+
#>
17+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
18+
'PSAvoidLongLines', '',
19+
Justification = 'Contains a long link.'
20+
)]
21+
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
22+
param(
23+
# The installation ID to remove.
24+
[Parameter(Mandatory)]
25+
[Alias('InstallationID')]
26+
[ValidateRange(1, [UInt64]::MaxValue)]
27+
[UInt64] $ID,
28+
29+
# The context to run the command in.
30+
[Parameter(Mandatory)]
31+
[ValidateNotNull()]
32+
[object] $Context
33+
)
34+
35+
begin {
36+
$stackPath = Get-PSCallStackPath
37+
Write-Debug "[$stackPath] - Start"
38+
Assert-GitHubContext -Context $Context -AuthType APP
39+
}
40+
41+
process {
42+
Write-Verbose "Uninstalling GitHub App Installation: $ID"
43+
44+
$apiParams = @{
45+
Method = 'DELETE'
46+
APIEndpoint = "/app/installations/$ID"
47+
Context = $Context
48+
}
49+
50+
if ($PSCmdlet.ShouldProcess("GitHub App Installation: $ID", 'Uninstall')) {
51+
$null = Invoke-GitHubAPI @apiParams
52+
Write-Verbose "Successfully removed installation: $ID"
53+
}
54+
}
55+
56+
end {
57+
Write-Debug "[$stackPath] - End"
58+
}
59+
}

src/functions/private/Apps/GitHub Apps/Uninstall-GitHubAppOnEnterpriseOrganization.ps1

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,46 @@
11
function Uninstall-GitHubAppOnEnterpriseOrganization {
22
<#
33
.SYNOPSIS
4-
Uninstall a GitHub App from an organization.
4+
Uninstall a GitHub App from an enterprise-owned organization.
55
66
.DESCRIPTION
7-
Uninstall a GitHub App from an organization.
7+
Uninstall a GitHub App from an enterprise-owned organization.
88
99
The authenticated GitHub App must be installed on the enterprise and be granted the Enterprise/organization_installations (write) permission.
1010
1111
.EXAMPLE
12-
Uninstall-GitHubAppOnEnterpriseOrganization -Enterprise 'github' -Organization 'octokit' -InstallationID '123456'
12+
Uninstall-GitHubAppOnEnterpriseOrganization -Enterprise 'github' -Organization 'octokit' -ID '123456'
1313
1414
Uninstall the GitHub App with the installation ID `123456` from the organization `octokit` in the enterprise `github`.
15+
16+
.NOTES
17+
[Uninstall a GitHub App from an enterprise-owned organization](https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/organization-installations#uninstall-a-github-app-from-an-enterprise-owned-organization)
1518
#>
16-
[CmdletBinding()]
19+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
20+
'PSAvoidLongLines', '',
21+
Justification = 'Contains a long link.'
22+
)]
23+
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
1724
param(
1825
# The enterprise slug or ID.
1926
[Parameter(Mandatory)]
27+
[ValidateNotNullOrEmpty()]
2028
[string] $Enterprise,
2129

2230
# The organization name. The name is not case sensitive.
2331
[Parameter(Mandatory)]
32+
[ValidateNotNullOrEmpty()]
2433
[string] $Organization,
2534

26-
# The client ID of the GitHub App to install.
35+
# The ID of the GitHub App installation to uninstall.
2736
[Parameter(Mandatory)]
28-
[string] $ID,
37+
[Alias('InstallationID')]
38+
[ValidateRange(1, [UInt64]::MaxValue)]
39+
[UInt64] $ID,
2940

3041
# The context to run the command in. Used to get the details for the API call.
3142
[Parameter(Mandatory)]
43+
[ValidateNotNull()]
3244
[object] $Context
3345
)
3446

@@ -40,14 +52,16 @@
4052
}
4153

4254
process {
55+
Write-Verbose "Uninstalling GitHub App [$Enterprise/$Organization/$ID]"
4356
$apiParams = @{
4457
Method = 'DELETE'
4558
APIEndpoint = "/enterprises/$Enterprise/apps/organizations/$Organization/installations/$ID"
4659
Context = $Context
4760
}
4861

49-
Invoke-GitHubAPI @apiParams | ForEach-Object {
50-
Write-Output $_.Response
62+
if ($PSCmdlet.ShouldProcess("GitHub App Installation: $Enterprise/$Organization/$ID", 'Uninstall')) {
63+
$null = Invoke-GitHubAPI @apiParams
64+
Write-Verbose "Successfully removed installation: $Enterprise/$Organization/$ID"
5165
}
5266
}
5367

src/functions/public/Apps/GitHub App/Uninstall-GitHubApp.ps1

Lines changed: 118 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,72 @@
44
Uninstall a GitHub App.
55
66
.DESCRIPTION
7-
Uninstalls the provided GitHub App on the specified target.
7+
Uninstalls a GitHub App installation. Works in two modes:
8+
- As the authenticated App (APP context): remove installations by target name, ID, or pipeline objects.
9+
- As an enterprise installation (IAT/UAT context with Enterprise): remove an app from an organization by InstallationID or AppSlug.
810
911
.EXAMPLE
10-
Uninstall-GitHubApp -Enterprise 'msx' -Organization 'org' -InstallationID '123456'
12+
Uninstall-GitHubApp -Target 'octocat'
13+
Uninstall-GitHubApp -Target 12345
1114
12-
Uninstall the GitHub App with the installation ID '123456' from the organization 'org' in the enterprise 'msx'.
15+
As an App: uninstall by target name (enterprise/org/user) or by exact installation ID
16+
17+
.EXAMPLE
18+
Get-GitHubAppInstallation | Uninstall-GitHubApp
19+
20+
As an App: uninstall using pipeline objects
21+
22+
.EXAMPLE
23+
Uninstall-GitHubApp -Organization 'org' -InstallationID 123456 -Context (Connect-GitHubApp -Enterprise 'msx' -PassThru)
24+
25+
As an enterprise installation: uninstall by installation ID in an org
26+
27+
.EXAMPLE
28+
Uninstall-GitHubApp -Organization 'org' -AppSlug 'my-app' -Context (Connect-GitHubApp -Enterprise 'msx' -PassThru)
29+
30+
As an enterprise installation: uninstall by app slug in an org
1331
1432
.LINK
1533
https://psmodule.io/GitHub/Functions/Apps/GitHub%20App/Uninstall-GitHubApp
1634
#>
17-
[CmdletBinding(DefaultParameterSetName = '__AllParameterSets')]
35+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidLongLines', '', Justification = 'Contains a long link.')]
36+
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High', DefaultParameterSetName = 'App-ByTarget')]
1837
param(
19-
# The enterprise slug or ID.
20-
[Parameter(
21-
Mandatory,
22-
ParameterSetName = 'EnterpriseOrganization',
23-
ValueFromPipelineByPropertyName
24-
)]
25-
[string] $Enterprise,
38+
# As APP: target to uninstall. Accepts a name (enterprise/org/user) or an installation ID.
39+
[Parameter(Mandatory, ParameterSetName = 'App-ByTarget', ValueFromPipelineByPropertyName)]
40+
[Parameter(ParameterSetName = 'App-ByObject')]
41+
[Alias('Name')]
42+
[object] $Target,
43+
44+
# As APP via pipeline: installation objects.
45+
[Parameter(Mandatory, ParameterSetName = 'App-ByObject', ValueFromPipeline)]
46+
[GitHubAppInstallation[]] $InstallationObject,
2647

27-
# The organization name. The name is not case sensitive.
28-
[Parameter(
29-
Mandatory,
30-
ParameterSetName = 'EnterpriseOrganization',
31-
ValueFromPipelineByPropertyName
32-
)]
48+
# As Enterprise (IAT/UAT): organization where the app is installed.
49+
[Parameter(Mandatory, ParameterSetName = 'Enterprise-ByID')]
50+
[Parameter(Mandatory, ParameterSetName = 'Enterprise-BySlug')]
51+
[ValidateNotNullOrEmpty()]
3352
[string] $Organization,
3453

35-
# The client ID of the GitHub App to install.
36-
[Parameter(
37-
Mandatory,
38-
ValueFromPipelineByPropertyName
39-
)]
40-
[Alias('installation_id', 'InstallationID')]
41-
[string] $ID,
54+
# As Enterprise (IAT/UAT): enterprise slug or ID. Optional if the context already has Enterprise set.
55+
[Parameter(ParameterSetName = 'Enterprise-ByID')]
56+
[Parameter(ParameterSetName = 'Enterprise-BySlug')]
57+
[ValidateNotNullOrEmpty()]
58+
[string] $Enterprise,
59+
60+
# As Enterprise (IAT/UAT): installation ID to remove.
61+
[Parameter(Mandatory, ParameterSetName = 'Enterprise-ByID')]
62+
[Alias('ID')]
63+
[ValidateRange(1, [UInt64]::MaxValue)]
64+
[UInt64] $InstallationID,
65+
66+
# As Enterprise (IAT/UAT): app slug to uninstall (when the installation ID is unknown).
67+
[Parameter(Mandatory, ParameterSetName = 'Enterprise-BySlug')]
68+
[Alias('Slug', 'AppName')]
69+
[ValidateNotNullOrEmpty()]
70+
[string] $AppSlug,
4271

43-
# The context to run the command in. Used to get the details for the API call.
44-
# Can be either a string or a GitHubContext object.
72+
# Common: explicit context (APP for app mode; IAT/UAT with Enterprise for enterprise mode)
4573
[Parameter()]
4674
[object] $Context
4775
)
@@ -50,18 +78,78 @@
5078
$stackPath = Get-PSCallStackPath
5179
Write-Debug "[$stackPath] - Start"
5280
$Context = Resolve-GitHubContext -Context $Context
81+
Assert-GitHubContext -Context $Context -AuthType APP, IAT, UAT
5382
}
5483

5584
process {
5685
switch ($PSCmdlet.ParameterSetName) {
57-
'EnterpriseOrganization' {
86+
'App-ByTarget' {
87+
if ($Context.AuthType -ne 'APP') {
88+
throw 'App-ByTarget requires APP authentication. Provide an App context or connect as an App.'
89+
}
90+
91+
$id = $null
92+
if ($Target -is [int] -or $Target -is [long] -or $Target -is [uint64]) { $id = [uint64]$Target }
93+
elseif ($Target -is [string] -and ($Target -as [uint64])) { $id = [uint64]$Target }
94+
95+
if ($id) {
96+
if ($PSCmdlet.ShouldProcess("GitHub App Installation: $id", 'Uninstall')) {
97+
Uninstall-GitHubAppAsApp -ID $id -Context $Context -Confirm:$false
98+
}
99+
return
100+
}
101+
102+
$installations = Get-GitHubAppInstallation -Context $Context
103+
$instMatches = $installations | Where-Object { $_.Target.Name -like "*$Target*" }
104+
if (-not $instMatches) { throw "No installations found matching target '$Target'." }
105+
foreach ($inst in $instMatches) {
106+
if ($PSCmdlet.ShouldProcess("GitHub App Installation: $($inst.ID) [$($inst.Target.Type)/$($inst.Target.Name)]", 'Uninstall')) {
107+
Uninstall-GitHubAppAsApp -ID $inst.ID -Context $Context -Confirm:$false
108+
}
109+
}
110+
}
111+
112+
'App-ByObject' {
113+
if ($Context.AuthType -ne 'APP') {
114+
throw 'App-ByObject requires APP authentication. Provide an App context or connect as an App.'
115+
}
116+
foreach ($inst in $InstallationObject) {
117+
if (-not $inst.ID) { continue }
118+
if ($PSCmdlet.ShouldProcess("GitHub App Installation: $($inst.ID) [$($inst.Target.Type)/$($inst.Target.Name)]", 'Uninstall')) {
119+
Uninstall-GitHubAppAsApp -ID $inst.ID -Context $Context -Confirm:$false
120+
}
121+
}
122+
}
123+
124+
'Enterprise-ByID' {
125+
$effectiveEnterprise = if ($Enterprise) { $Enterprise } else { $Context.Enterprise }
126+
if (-not $effectiveEnterprise) { throw 'Enterprise-ByID requires an enterprise to be specified (via -Enterprise or Context.Enterprise).' }
58127
$params = @{
59-
Enterprise = $Enterprise
128+
Enterprise = $effectiveEnterprise
60129
Organization = $Organization
61-
ID = $ID
130+
ID = $InstallationID
62131
Context = $Context
63132
}
64-
Uninstall-GitHubAppOnEnterpriseOrganization @params
133+
if ($PSCmdlet.ShouldProcess("GitHub App Installation: $effectiveEnterprise/$Organization/$InstallationID", 'Uninstall')) {
134+
Uninstall-GitHubAppOnEnterpriseOrganization @params -Confirm:$false
135+
}
136+
}
137+
138+
'Enterprise-BySlug' {
139+
$effectiveEnterprise = if ($Enterprise) { $Enterprise } else { $Context.Enterprise }
140+
if (-not $effectiveEnterprise) { throw 'Enterprise-BySlug requires an enterprise to be specified (via -Enterprise or Context.Enterprise).' }
141+
$inst = Get-GitHubEnterpriseOrganizationAppInstallation -Enterprise $effectiveEnterprise -Organization $Organization -Context $Context |
142+
Where-Object { $_.App.Slug -eq $AppSlug } | Select-Object -First 1
143+
if (-not $inst) { throw "No installation found for app slug '$AppSlug' in org '$Organization'." }
144+
$params = @{
145+
Enterprise = $effectiveEnterprise
146+
Organization = $Organization
147+
ID = $inst.ID
148+
Context = $Context
149+
}
150+
if ($PSCmdlet.ShouldProcess("GitHub App Installation: $effectiveEnterprise/$Organization/$($inst.ID) (app '$AppSlug')", 'Uninstall')) {
151+
Uninstall-GitHubAppOnEnterpriseOrganization @params -Confirm:$false
152+
}
65153
}
66154
}
67155
}
@@ -70,5 +158,3 @@
70158
Write-Debug "[$stackPath] - End"
71159
}
72160
}
73-
74-
#SkipTest:FunctionTest:Will add a test for this function in a future PR

0 commit comments

Comments
 (0)