diff --git a/.github/PSModule.yml b/.github/PSModule.yml index ed2d635e9..6d578178e 100644 --- a/.github/PSModule.yml +++ b/.github/PSModule.yml @@ -1,3 +1,3 @@ Test: CodeCoverage: - PercentTarget: 40 + PercentTarget: 50 diff --git a/Coverage.md b/Coverage.md index 82f272f63..f39f6188a 100644 --- a/Coverage.md +++ b/Coverage.md @@ -5,19 +5,19 @@ - + - + - + - +
Available functions10261028
Covered functions221220
Missing functions805808
Coverage21.54%21.4%
@@ -51,6 +51,7 @@ | `/classrooms/{classroom_id}/assignments` | | :x: | | | | | `/codes_of_conduct` | | :x: | | | | | `/codes_of_conduct/{key}` | | :x: | | | | +| `/credentials/revoke` | | | | :x: | | | `/emojis` | | :white_check_mark: | | | | | `/enterprises/{enterprise}/code-security/configurations` | | :x: | | :x: | | | `/enterprises/{enterprise}/code-security/configurations/defaults` | | :x: | | | | @@ -539,10 +540,10 @@ | `/repos/{owner}/{repo}/releases` | | :white_check_mark: | | :white_check_mark: | | | `/repos/{owner}/{repo}/releases/assets/{asset_id}` | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | | `/repos/{owner}/{repo}/releases/generate-notes` | | | | :white_check_mark: | | -| `/repos/{owner}/{repo}/releases/latest` | | :white_check_mark: | | | | -| `/repos/{owner}/{repo}/releases/tags/{tag}` | | :white_check_mark: | | | | +| `/repos/{owner}/{repo}/releases/latest` | | :x: | | | | +| `/repos/{owner}/{repo}/releases/tags/{tag}` | | :x: | | | | | `/repos/{owner}/{repo}/releases/{release_id}` | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | -| `/repos/{owner}/{repo}/releases/{release_id}/assets` | | :white_check_mark: | | :x: | | +| `/repos/{owner}/{repo}/releases/{release_id}/assets` | | :white_check_mark: | | :white_check_mark: | | | `/repos/{owner}/{repo}/releases/{release_id}/reactions` | | :x: | | :x: | | | `/repos/{owner}/{repo}/releases/{release_id}/reactions/{reaction_id}` | :x: | | | | | | `/repos/{owner}/{repo}/rules/branches/{branch}` | | :x: | | | | @@ -698,6 +699,7 @@ | `/users/{username}/settings/billing/actions` | | :x: | | | | | `/users/{username}/settings/billing/packages` | | :x: | | | | | `/users/{username}/settings/billing/shared-storage` | | :x: | | | | +| `/users/{username}/settings/billing/usage` | | :x: | | | | | `/users/{username}/social_accounts` | | :white_check_mark: | | | | | `/users/{username}/ssh_signing_keys` | | :white_check_mark: | | | | | `/users/{username}/starred` | | :x: | | | | diff --git a/examples/Releases/Releases.ps1 b/examples/Releases/Releases.ps1 new file mode 100644 index 000000000..1b38c9adf --- /dev/null +++ b/examples/Releases/Releases.ps1 @@ -0,0 +1,60 @@ +# Get all the releases for a specific repository +Get-GitHubRelease -Owner PSModule -Repository GitHub + +# Get the latest release for a specific repository +Get-GitHubRelease -Owner PSModule -Repository GitHub + +# Get all the releases for all repos in the organization +'PSModule' | Get-GitHubOrganization | Get-GitHubRepository | Get-GitHubRelease + +# Get the latest releases for all repos in the organization +'PSModule' | Get-GitHubOrganization | Get-GitHubRepository | ForEach-Object -ThrottleLimit ([Environment]::ProcessorCount) -Parallel { + do { + Import-Module -Name GitHub + } until ($? -eq $true) + $_ | Get-GitHubRelease +} + +'PSModule' | Get-GitHubOrganization | Get-GitHubRepository | Get-GitHubRelease -Latest +Get-GitHubUser | Get-GitHubRepository | Get-GitHubRelease -Latest + +# Create a new release for a specific repository +$repoName = 'mytest' + +$repo = Get-GitHubUser | Get-GitHubRepository -Name $repoName +$repo | Get-GitHubRelease +$repo | New-GitHubRelease -Tag 'v1.0' -Latest +$repo | New-GitHubRelease -Tag 'v1.1' -Latest -Name 'test' +$repo | New-GitHubRelease -Tag 'v1.2' -Latest -Name 'test' -Notes 'Release notes' +$repo | New-GitHubRelease -Tag 'v1.3' -Latest -Name 'test' -GenerateReleaseNotes +$repo | Get-GitHubRelease -Tag 'v1.3' | Format-List +$repo | Get-GitHubRelease -Tag 'v1.3' | Update-GithubRelease -Notes 'Release notes1' +$repo | Update-GitHubRelease -Tag 'v1.3' -Name 'test123' -Debug + +$repo | New-GitHubRelease -Tag 'v1.3.2' -Latest -GenerateReleaseNotes -Debug +$repo | Get-GitHubRelease -Tag 'v1.3.1' | Format-List +$repo | Set-GitHubRelease -Tag 'v1.3.1' -Latest -GenerateReleaseNotes -Debug +$repo | Set-GitHubRelease -Tag 'v1.3.1' -Latest -GenerateReleaseNotes -Notes 'Release notes' +$repo | Get-GitHubRelease -Tag 'v1.3.1' | Format-List +Get-GitHubReleaseAsset -Owner MariusStorhaug -Repository mytest -ReleaseID + +$repo | Set-GitHubRelease -Tag 'v1.5' -Latest -Name 'test' -Notes 'Release notes' | Select-Object * +$repo | Get-GitHubRelease -Tag 'v1.4' | Select-Object Tag, Name, Latest, Prerelease, Draft +$repo | Set-GitHubRelease -Tag 'v1.4' | Select-Object Tag, Name, Latest, Prerelease, Draft +$repo | Set-GitHubRelease -Tag 'v1.4' -Name 'test2' -Draft | Select-Object Tag, Name, Latest, Prerelease, Draft +$repo | Set-GitHubRelease -Tag 'v1.4' -Name 'test2' -Draft -Prerelease | Select-Object Tag, Name, Latest, Prerelease, Draft +$repo | Set-GitHubRelease -Tag 'v1.4' -Name 'test2' -Prerelease | Select-Object Tag, Name, Latest, Prerelease, Draft +$repo | Set-GitHubRelease -Tag 'v1.4' -Name 'test2' -Latest | Select-Object Tag, Name, Latest, Prerelease, Draft +$repo | Set-GitHubRelease -Tag 'v1.4' -Name 'test2' | Select-Object Tag, Name, Latest, Prerelease, Draft + +$repo | Get-GitHubRelease -Tag 'v1.4' | Select-Object * + +$repo | Set-GitHubRelease -Tag 'v1.4' -Name 'test2' + + +New-GitHubReleaseNote -Owner PSModule -Repository GitHub -Tag 'v0.22.0' -Target 'main' -PreviousTag 'v0.20.0' | Format-List + +$repo | Get-GitHubRelease | Remove-GitHubRelease + + +$repo | Remove-GitHubRepository -Confirm:$false diff --git a/src/classes/public/Releases/GitHubRelease.ps1 b/src/classes/public/Releases/GitHubRelease.ps1 new file mode 100644 index 000000000..8ef7adbaf --- /dev/null +++ b/src/classes/public/Releases/GitHubRelease.ps1 @@ -0,0 +1,93 @@ +class GitHubRelease : GitHubNode { + # Name of the release, can be null + # Example: "v0.22.1" + [string] $Name + + # The repository where the environment is. + [string] $Repository + + # The owner of the environment. + [string] $Owner + + # The name of the tag + # Example: "v0.22.1" + [string] $Tag + + # Release notes or changelog, can be null + # Example: "## What's Changed\n### Other Changes\n* Fix: Enhance repository deletion feedback and fix typo..." + [string] $Notes + + # Specifies the commitish value that determines where the Git tag is created from + # Example: "main" + [string] $Target + + # True if the release is the latest release on the repo. + # Example: true + [bool] $IsLatest + + # True to create a draft (unpublished) release, false to create a published one + # Example: false + [bool] $IsDraft + + # Whether to identify the release as a prerelease or a full release + # Example: false + [bool] $IsPrerelease + + # GitHub URL for the release + # Example: "https://github.com/PSModule/GitHub/releases/tag/v0.22.1" + [string] $Url + + # User who authored the release + [GitHubUser] $Author + + # Timestamp when the release was created + # Example: "2025-04-11T09:03:38Z" + [System.Nullable[datetime]] $CreatedAt + + # Timestamp when the release was published + # Example: "2025-04-11T13:41:34Z" + [System.Nullable[datetime]] $PublishedAt + + # Timestamp when the release was updated + # Example: "2025-04-11T13:41:34Z" + [System.Nullable[datetime]] $UpdatedAt + + GitHubRelease() {} + + GitHubRelease([PSCustomObject] $Object, [string] $Owner, [string] $Repository, [System.Nullable[bool]] $Latest) { + if ($null -ne $Object.node_id) { + $this.ID = $Object.id + $this.NodeID = $Object.node_id + $this.Tag = $Object.tag_name + $this.Name = $Object.name + $this.Notes = $Object.body + $this.IsLatest = $Latest + $this.IsDraft = $Object.draft + $this.IsPrerelease = $Object.prerelease + $this.Url = $Object.html_url + $this.Owner = $Owner + $this.Repository = $Repository + $this.Target = $Object.target_commitish + $this.CreatedAt = $Object.created_at + $this.PublishedAt = $Object.published_at + $this.Author = [GitHubUser]::new($Object.author) + } else { + $this.ID = $Object.databaseId + $this.NodeID = $Object.id + $this.Tag = $Object.tagName + $this.Name = $Object.name + $this.Notes = $Object.description + $this.IsLatest = $Object.isLatest + $this.IsDraft = $Object.isDraft + $this.IsPrerelease = $Object.isPrerelease + $this.Url = $Object.url + $this.Owner = $Owner + $this.Repository = $Repository + # $this.Target = $Object.target_commitish + $this.CreatedAt = $Object.createdAt + $this.PublishedAt = $Object.publishedAt + $this.UpdatedAt = $Object.updatedAt + $this.Author = [GitHubUser]::new($Object.author) + } + } +} diff --git a/src/classes/public/Releases/GitHubReleaseAsset.ps1 b/src/classes/public/Releases/GitHubReleaseAsset.ps1 new file mode 100644 index 000000000..c0ae6d74b --- /dev/null +++ b/src/classes/public/Releases/GitHubReleaseAsset.ps1 @@ -0,0 +1,70 @@ +class GitHubReleaseAsset : GitHubNode { + # Description: URL for downloading the asset + # Example: "https://github.com/PSModule/GitHub/releases/download/v0.22.1/asset.zip" + [string] $Url + + # Description: The file name of the asset + # Example: "Team Environment" + [string] $Name + + # Description: Label for the asset, can be null + # Example: null + [string] $Label + + # Description: State of the release asset (e.g., uploaded, open) + # Example: "uploaded" + [string] $State + + # Description: MIME type of the asset + # Example: "application/zip" + [string] $ContentType + + # Description: Size of the asset in bytes + # Example: 1024 + [int] $Size + + # Description: Number of times the asset was downloaded + # Example: 100 + [int] $Downloads + + # Description: Timestamp when the asset was created + # Example: "2025-04-11T09:03:38Z" + [System.Nullable[datetime]] $CreatedAt + + # Description: Timestamp when the asset was last updated + # Example: "2025-04-11T09:03:38Z" + [System.Nullable[datetime]] $UpdatedAt + + # Description: User who uploaded the asset, can be null + # Example: GitHubUser object or null + [GitHubUser] $UploadedBy + + GitHubReleaseAsset() {} + + GitHubReleaseAsset([PSCustomObject]$Object) { + if ($null -ne $Object.node_id) { + $this.ID = $Object.id + $this.NodeID = $Object.node_id + $this.Url = $Object.browser_download_url + $this.Name = $Object.name + $this.Label = $Object.label + $this.State = $Object.state + $this.ContentType = $Object.content_type + $this.Size = $Object.size + $this.Downloads = $Object.download_count + $this.CreatedAt = [datetime]::Parse($Object.created_at) + $this.UpdatedAt = [datetime]::Parse($Object.updated_at) + $this.UploadedBy = [GitHubUser]::new($Object.uploader) + } else { + $this.NodeID = $Object.id + $this.Url = $Object.downloadUrl + $this.Name = $Object.name + $this.ContentType = $Object.contentType + $this.Size = $Object.size + $this.Downloads = $Object.downloadCount + $this.CreatedAt = [datetime]::Parse($Object.createdAt) + $this.UpdatedAt = [datetime]::Parse($Object.updatedAt) + $this.UploadedBy = [GitHubUser]::new($Object.uploadedBy) + } + } +} diff --git a/src/formats/GitHubRelease.Format.ps1xml b/src/formats/GitHubRelease.Format.ps1xml new file mode 100644 index 000000000..223919863 --- /dev/null +++ b/src/formats/GitHubRelease.Format.ps1xml @@ -0,0 +1,119 @@ + + + + + GitHubReleaseTable + + GitHubRelease + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag + + + Owner + + + Repository + + + Url + + + IsLatest + + + IsPrerelease + + + IsDraft + + + + + + + + GitHubReleaseList + + GitHubRelease + + + + + + + Tag + + + Owner + + + Repository + + + Url + + + Author + + + Target + + + IsLatest + + + IsDraft + + + IsPrerelease + + + CreatedAt + + + PublishedAt + + + Assets + + + Name + + + Notes + + + + + + + + diff --git a/src/functions/private/Auth/Context/Resolve-GitHubContextSetting.ps1 b/src/functions/private/Auth/Context/Resolve-GitHubContextSetting.ps1 index bcfc2f646..b152c6e96 100644 --- a/src/functions/private/Auth/Context/Resolve-GitHubContextSetting.ps1 +++ b/src/functions/private/Auth/Context/Resolve-GitHubContextSetting.ps1 @@ -58,6 +58,10 @@ [object] $Context ) + if ($Name -eq 'PerPage' -and $Value -eq 0) { + $Value = $null + } + Write-Debug "Resolving setting [$Name]" [pscustomobject]@{ 'Name' = $Name diff --git a/src/functions/private/Releases/Assets/Get-GitHubReleaseAssetByID.ps1 b/src/functions/private/Releases/Assets/Get-GitHubReleaseAssetByID.ps1 index e7a739fba..d2a53b506 100644 --- a/src/functions/private/Releases/Assets/Get-GitHubReleaseAssetByID.ps1 +++ b/src/functions/private/Releases/Assets/Get-GitHubReleaseAssetByID.ps1 @@ -1,7 +1,7 @@ filter Get-GitHubReleaseAssetByID { <# .SYNOPSIS - Get a release asset + Get a release asset by ID .DESCRIPTION To download the asset's binary content, set the `Accept` header of the request to @@ -14,10 +14,13 @@ Gets the release asset with the ID '1234567' for the repository 'octocat/hello-world'. + .OUTPUTS + GitHubReleaseAsset + .NOTES https://docs.github.com/rest/releases/assets#get-a-release-asset - #> + [OutputType([GitHubReleaseAsset])] [CmdletBinding()] param( # The account owner of the repository. The name is not case sensitive. @@ -30,7 +33,6 @@ # The unique identifier of the asset. [Parameter(Mandatory)] - [Alias('asset_id')] [string] $ID, # The context to run the command in. Used to get the details for the API call. @@ -53,7 +55,7 @@ } Invoke-GitHubAPI @inputObject | ForEach-Object { - Write-Output $_.Response + [GitHubReleaseAsset]($_.Response) } } diff --git a/src/functions/private/Releases/Assets/Get-GitHubReleaseAssetByReleaseID.ps1 b/src/functions/private/Releases/Assets/Get-GitHubReleaseAssetByReleaseID.ps1 index 512f93e4a..e1d7f6a56 100644 --- a/src/functions/private/Releases/Assets/Get-GitHubReleaseAssetByReleaseID.ps1 +++ b/src/functions/private/Releases/Assets/Get-GitHubReleaseAssetByReleaseID.ps1 @@ -11,11 +11,16 @@ Gets the release assets for the release with the ID '1234567' for the repository 'octocat/hello-world'. + .EXAMPLE + Get-GitHubReleaseAssetByReleaseID -Owner 'octocat' -Repository 'hello-world' -ID '1234567' -Name 'example.zip' + + Gets only the release asset named 'example.zip' for the release with the ID '1234567'. + .NOTES https://docs.github.com/rest/releases/assets#list-release-assets #> - [CmdletBinding(DefaultParameterSetName = '__AllParameterSets')] + [CmdletBinding()] param( # The account owner of the repository. The name is not case sensitive. [Parameter(Mandatory)] @@ -26,13 +31,13 @@ [string] $Repository, # The unique identifier of the release. - [Parameter( - Mandatory, - ParameterSetName = 'ID' - )] - [Alias('release_id')] + [Parameter(Mandatory)] [string] $ID, + # The name of a specific asset to return. If provided, only the asset with this name will be returned. + [Parameter()] + [string] $Name, + # The number of results per page (max 100). [Parameter()] [ValidateRange(0, 100)] @@ -63,7 +68,16 @@ } Invoke-GitHubAPI @inputObject | ForEach-Object { - Write-Output $_.Response + foreach ($asset in $_.Response) { + if ($PSBoundParameters.ContainsKey('Name')) { + if ($asset.name -eq $Name) { + [GitHubReleaseAsset]($asset) + break + } + } else { + [GitHubReleaseAsset]($asset) + } + } } } end { diff --git a/src/functions/private/Releases/Assets/Get-GitHubReleaseAssetByTag.ps1 b/src/functions/private/Releases/Assets/Get-GitHubReleaseAssetByTag.ps1 new file mode 100644 index 000000000..8902da267 --- /dev/null +++ b/src/functions/private/Releases/Assets/Get-GitHubReleaseAssetByTag.ps1 @@ -0,0 +1,128 @@ +filter Get-GitHubReleaseAssetByTag { + <# + .SYNOPSIS + Get release assets by tag name + + .DESCRIPTION + Gets all assets from a release identified by its tag name. + Uses pagination to retrieve all assets even if there are more than the maximum per page. + + .EXAMPLE + Get-GitHubReleaseAssetByTag -Owner 'octocat' -Repository 'hello-world' -Tag 'v1.0.0' + + Gets all release assets for the release with the tag 'v1.0.0' for the repository 'octocat/hello-world'. + + .EXAMPLE + Get-GitHubReleaseAssetByTag -Owner 'octocat' -Repository 'hello-world' -Tag 'v1.0.0' -Name 'app.zip' + + Gets a specific release asset named 'app.zip' from the release with the tag 'v1.0.0' for the repository 'octocat/hello-world'. + + .OUTPUTS + GitHubReleaseAsset + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', 'hasNextPage', Scope = 'Function', + Justification = 'Unknown issue with var scoping in blocks.' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', 'after', Scope = 'Function', + Justification = 'Unknown issue with var scoping in blocks.' + )] + [OutputType([GitHubReleaseAsset])] + [CmdletBinding()] + param( + # The account owner of the repository. The name is not case sensitive. + [Parameter(Mandatory)] + [string] $Owner, + + # The name of the repository without the .git extension. The name is not case sensitive. + [Parameter(Mandatory)] + [string] $Repository, + + # The name of the tag to get a release from. + [Parameter(Mandatory)] + [string] $Tag, + + # The name of the asset to get. If specified, only assets with this name will be returned. + [Parameter()] + [string] $Name, + + # The number of results per page (max 100). + [Parameter()] + [ValidateRange(0, 100)] + [int] $PerPage, + + # 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" + Assert-GitHubContext -Context $Context -AuthType IAT, PAT, UAT + } + + process { + $hasNextPage = $true + $after = $null + $nameParam = $PSBoundParameters.ContainsKey('Name') ? ", name: ""$Name""" : '' + $perPageSetting = Resolve-GitHubContextSetting -Name 'PerPage' -Value $PerPage -Context $Context + + do { + $inputObject = @{ + Query = @" +query(`$owner: String!, `$repository: String!, `$tag: String!, `$perPage: Int, `$after: String) { + repository(owner: `$owner, name: `$repository) { + release(tagName: `$tag) { + releaseAssets(first: `$perPage, after: `$after$nameParam) { + nodes { + id + name + contentType + size + downloadCount + downloadUrl + url + createdAt + updatedAt + uploadedBy { + login + } + } + pageInfo { + endCursor + hasNextPage + } + } + } + } +} +"@ + Variables = @{ + owner = $Owner + repository = $Repository + tag = $Tag + perPage = $perPageSetting + after = $after + } + Context = $Context + } + + Invoke-GitHubGraphQLQuery @inputObject | ForEach-Object { + $release = $_.repository.release + $assets = $release.releaseAssets + foreach ($asset in $assets.nodes) { + [GitHubReleaseAsset]::new($asset) + } + $hasNextPage = $assets.pageInfo.hasNextPage + $after = $assets.pageInfo.endCursor + } + } while ($hasNextPage) + } + + end { + Write-Debug "[$stackPath] - End" + } +} diff --git a/src/functions/private/Releases/Assets/Get-GitHubReleaseAssetFromLatest.ps1 b/src/functions/private/Releases/Assets/Get-GitHubReleaseAssetFromLatest.ps1 new file mode 100644 index 000000000..95d40d520 --- /dev/null +++ b/src/functions/private/Releases/Assets/Get-GitHubReleaseAssetFromLatest.ps1 @@ -0,0 +1,127 @@ +filter Get-GitHubReleaseAssetFromLatest { + <# + .SYNOPSIS + Get the assets of the latest release + + .DESCRIPTION + Gets all assets for the latest published full release for the repository. + The latest release is the most recent non-prerelease, non-draft release, sorted by the `created_at` attribute. + The `created_at` attribute is the date of the commit used for the release, and not the date when the release was drafted or published. + + .EXAMPLE + Get-GitHubReleaseAssetFromLatest -Owner 'octocat' -Repository 'hello-world' + + Gets the assets for the latest release of the repository 'hello-world' owned by 'octocat'. + + .EXAMPLE + Get-GitHubReleaseAssetFromLatest -Owner 'octocat' -Repository 'hello-world' -Name 'asset-name' + + Gets the assets for the latest release of the repository 'hello-world' owned by 'octocat'. + + .INPUTS + GitHubRepository + + .OUTPUTS + GitHubReleaseAsset + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', 'hasNextPage', Scope = 'Function', + Justification = 'Unknown issue with var scoping in blocks.' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', 'after', Scope = 'Function', + Justification = 'Unknown issue with var scoping in blocks.' + )] + [OutputType([GitHubReleaseAsset])] + [CmdletBinding()] + param( + # The account owner of the repository. The name is not case sensitive. + [Parameter(Mandatory)] + [string] $Owner, + + # The name of the repository without the .git extension. The name is not case sensitive. + [Parameter(Mandatory)] + [string] $Repository, + + # The name of the asset to get. If specified, only assets with this name will be returned. + [Parameter()] + [string] $Name, + + # The number of results per page (max 100). + [Parameter()] + [ValidateRange(0, 100)] + [int] $PerPage, + + # 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" + Assert-GitHubContext -Context $Context -AuthType IAT, PAT, UAT + } + + process { + $hasNextPage = $true + $after = $null + $nameParam = $PSBoundParameters.ContainsKey('Name') ? ", name: x$Name""" : '' + $perPageSetting = Resolve-GitHubContextSetting -Name 'PerPage' -Value $PerPage -Context $Context + + do { + $inputObject = @{ + Query = @" +query(`$owner: String!, `$repository: String!, `$perPage: Int, `$after: String) { + repository(owner: `$owner, name: `$repository) { + latestRelease { + releaseAssets(first: `$perPage, after: `$after$nameParam) { + nodes { + id + name + contentType + size + downloadCount + downloadUrl + url + createdAt + updatedAt + uploadedBy { + login + } + } + pageInfo { + endCursor + hasNextPage + } + } + } + } +} +"@ + Variables = @{ + owner = $Owner + repository = $Repository + perPage = $perPageSetting + after = $after + } + Context = $Context + } + + Invoke-GitHubGraphQLQuery @inputObject | ForEach-Object { + $release = $_.repository.latestRelease + $assets = $release.releaseAssets + foreach ($asset in $assets.nodes) { + [GitHubReleaseAsset]::new($asset) + } + $hasNextPage = $assets.pageInfo.hasNextPage + $after = $assets.pageInfo.endCursor + } + } while ($hasNextPage) + } + + end { + Write-Debug "[$stackPath] - End" + } +} diff --git a/src/functions/private/Releases/Get-GitHubReleaseAll.ps1 b/src/functions/private/Releases/Get-GitHubReleaseAll.ps1 new file mode 100644 index 000000000..f065ae156 --- /dev/null +++ b/src/functions/private/Releases/Get-GitHubReleaseAll.ps1 @@ -0,0 +1,119 @@ +filter Get-GitHubReleaseAll { + <# + .SYNOPSIS + List releases + + .DESCRIPTION + This returns a list of releases, which does not include regular Git tags that have not been associated with a release. + To get a list of Git tags, use the [Repository Tags API](https://docs.github.com/rest/repos/repos#list-repository-tags). + Information about published releases are available to everyone. Only users with push access will receive listings for draft releases. + + .EXAMPLE + Get-GitHubReleaseAll -Owner 'octocat' -Repository 'hello-world' + + Gets all the releases for the repository 'hello-world' owned by 'octocat'. + + .INPUTS + GitHubRepository + + .OUTPUTS + GitHubRelease + + .LINK + [List releases](https://docs.github.com/rest/releases/releases#list-releases) + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', 'hasNextPage', Scope = 'Function', + Justification = 'Unknown issue with var scoping in blocks.' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', 'after', Scope = 'Function', + Justification = 'Unknown issue with var scoping in blocks.' + )] + [OutputType([GitHubRelease])] + [CmdletBinding(SupportsPaging)] + param( + # The account owner of the repository. The name is not case sensitive. + [Parameter(Mandatory)] + [string] $Owner, + + # The name of the repository without the .git extension. The name is not case sensitive. + [Parameter(Mandatory)] + [string] $Repository, + + # The number of results per page (max 100). + [Parameter()] + [ValidateRange(0, 100)] + [int] $PerPage, + + # 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" + Assert-GitHubContext -Context $Context -AuthType IAT, PAT, UAT + } + + process { + $hasNextPage = $true + $after = $null + $perPageSetting = Resolve-GitHubContextSetting -Name 'PerPage' -Value $PerPage -Context $Context + + do { + $inputObject = @{ + Query = @' +query($owner: String!, $repository: String!, $perPage: Int, $after: String) { + repository(owner: $owner, name: $repository) { + releases(first: $perPage, after: $after) { + nodes { + id + databaseId + tagName + name + description + isLatest + isDraft + isPrerelease + url + createdAt + publishedAt + updatedAt + author { + login + } + } + pageInfo { + endCursor + hasNextPage + } + } + } +} +'@ + Variables = @{ + owner = $Owner + repository = $Repository + perPage = $perPageSetting + after = $after + } + Context = $Context + } + + Invoke-GitHubGraphQLQuery @inputObject | ForEach-Object { + foreach ($release in $_.repository.releases.nodes) { + [GitHubRelease]::new($release, $Owner, $Repository, $null) + } + $hasNextPage = $_.repository.releases.pageInfo.hasNextPage + $after = $_.repository.releases.pageInfo.endCursor + } + } while ($hasNextPage) + } + + end { + Write-Debug "[$stackPath] - End" + } +} diff --git a/src/functions/private/Releases/Releases/Get-GitHubReleaseByID.ps1 b/src/functions/private/Releases/Get-GitHubReleaseByID.ps1 similarity index 73% rename from src/functions/private/Releases/Releases/Get-GitHubReleaseByID.ps1 rename to src/functions/private/Releases/Get-GitHubReleaseByID.ps1 index dc0c5c030..350bed5f1 100644 --- a/src/functions/private/Releases/Releases/Get-GitHubReleaseByID.ps1 +++ b/src/functions/private/Releases/Get-GitHubReleaseByID.ps1 @@ -12,10 +12,16 @@ Gets the release with the ID '1234567' for the repository 'hello-world' owned by 'octocat'. - .NOTES - https://docs.github.com/rest/releases/releases#get-a-release + .INPUTS + GitHubRepository + .OUTPUTS + GitHubRelease + + .LINK + [Get a release](https://docs.github.com/rest/releases/releases#get-a-release) #> + [OutputType([GitHubRelease])] [CmdletBinding()] param( # The account owner of the repository. The name is not case sensitive. @@ -28,7 +34,6 @@ # The unique identifier of the release. [Parameter(Mandatory)] - [Alias('release_id')] [string] $ID, # The context to run the command in. Used to get the details for the API call. @@ -44,15 +49,20 @@ } process { + $latest = Get-GitHubReleaseLatest -Owner $Owner -Repository $Repository -Context $Context + $inputObject = @{ Method = 'GET' APIEndpoint = "/repos/$Owner/$Repository/releases/$ID" Context = $Context } - Invoke-GitHubAPI @inputObject | ForEach-Object { - Write-Output $_.Response - } + try { + Invoke-GitHubAPI @inputObject | ForEach-Object { + $isLatest = $_.Response.id -eq $latest.id + [GitHubRelease]::new($_.Response, $Owner, $Repository, $isLatest) + } + } catch { return } } end { diff --git a/src/functions/private/Releases/Releases/Get-GitHubReleaseByTagName.ps1 b/src/functions/private/Releases/Get-GitHubReleaseByTagName.ps1 similarity index 56% rename from src/functions/private/Releases/Releases/Get-GitHubReleaseByTagName.ps1 rename to src/functions/private/Releases/Get-GitHubReleaseByTagName.ps1 index 519cf54f4..cad6a7d30 100644 --- a/src/functions/private/Releases/Releases/Get-GitHubReleaseByTagName.ps1 +++ b/src/functions/private/Releases/Get-GitHubReleaseByTagName.ps1 @@ -11,10 +11,16 @@ Gets the release with the tag 'v1.0.0' for the repository 'hello-world' owned by 'octocat'. - .NOTES - https://docs.github.com/rest/releases/releases#get-a-release-by-tag-name + .INPUTS + GitHubRepository + .OUTPUTS + GitHubRelease + + .LINK + [Get a release by tag name](https://docs.github.com/rest/releases/releases#get-a-release-by-tag-name) #> + [OutputType([GitHubRelease])] [CmdletBinding()] param( # The account owner of the repository. The name is not case sensitive. @@ -27,7 +33,6 @@ # The name of the tag to get a release from. [Parameter(Mandatory)] - [Alias('tag_name')] [string] $Tag, # The context to run the command in. Used to get the details for the API call. @@ -44,13 +49,42 @@ process { $inputObject = @{ - Method = 'GET' - APIEndpoint = "/repos/$Owner/$Repository/releases/tags/$Tag" - Context = $Context + Query = @' +query($owner: String!, $repository: String!, $tag: String!) { + repository(owner: $owner, name: $repository) { + release(tagName: $tag) { + id + databaseId + tagName + name + description + isLatest + isDraft + isPrerelease + url + createdAt + publishedAt + updatedAt + author { + login + } + } + } +} +'@ + Variables = @{ + owner = $Owner + repository = $Repository + tag = $Tag + } + Context = $Context } - Invoke-GitHubAPI @inputObject | ForEach-Object { - Write-Output $_.Response + Invoke-GitHubGraphQLQuery @inputObject | ForEach-Object { + $release = $_.repository.release + if ($release) { + [GitHubRelease]::new($release, $Owner, $Repository, $null) + } } } diff --git a/src/functions/private/Releases/Releases/Get-GitHubReleaseLatest.ps1 b/src/functions/private/Releases/Get-GitHubReleaseLatest.ps1 similarity index 59% rename from src/functions/private/Releases/Releases/Get-GitHubReleaseLatest.ps1 rename to src/functions/private/Releases/Get-GitHubReleaseLatest.ps1 index 80f7a6936..d4421fd51 100644 --- a/src/functions/private/Releases/Releases/Get-GitHubReleaseLatest.ps1 +++ b/src/functions/private/Releases/Get-GitHubReleaseLatest.ps1 @@ -13,10 +13,16 @@ Gets the latest releases for the repository 'hello-world' owned by 'octocat'. - .NOTES - https://docs.github.com/rest/releases/releases#get-the-latest-release + .INPUTS + GitHubRepository + .OUTPUTS + GitHubRelease + + .LINK + [Get the latest release](https://docs.github.com/rest/releases/releases#get-the-latest-release) #> + [OutputType([GitHubRelease])] [CmdletBinding()] param( # The account owner of the repository. The name is not case sensitive. @@ -41,13 +47,41 @@ process { $inputObject = @{ - Method = 'GET' - APIEndpoint = "/repos/$Owner/$Repository/releases/latest" - Context = $Context + Query = @' +query($owner: String!, $repository: String!) { + repository(owner: $owner, name: $repository) { + latestRelease { + id + databaseId + tagName + name + description + isLatest + isDraft + isPrerelease + url + createdAt + publishedAt + updatedAt + author { + login + } + } + } +} +'@ + Variables = @{ + owner = $Owner + repository = $Repository + } + Context = $Context } - Invoke-GitHubAPI @inputObject | ForEach-Object { - Write-Output $_.Response + Invoke-GitHubGraphQLQuery @inputObject | ForEach-Object { + $release = $_.repository.latestRelease + if ($release) { + [GitHubRelease]::new($release, $Owner, $Repository, $null) + } } } diff --git a/src/functions/private/Releases/Releases/Get-GitHubReleaseAll.ps1 b/src/functions/private/Releases/Releases/Get-GitHubReleaseAll.ps1 deleted file mode 100644 index bd5c52614..000000000 --- a/src/functions/private/Releases/Releases/Get-GitHubReleaseAll.ps1 +++ /dev/null @@ -1,66 +0,0 @@ -filter Get-GitHubReleaseAll { - <# - .SYNOPSIS - List releases - - .DESCRIPTION - This returns a list of releases, which does not include regular Git tags that have not been associated with a release. - To get a list of Git tags, use the [Repository Tags API](https://docs.github.com/rest/repos/repos#list-repository-tags). - Information about published releases are available to everyone. Only users with push access will receive listings for draft releases. - - .EXAMPLE - Get-GitHubReleaseAll -Owner 'octocat' -Repository 'hello-world' - - Gets all the releases for the repository 'hello-world' owned by 'octocat'. - - .NOTES - https://docs.github.com/rest/releases/releases#list-releases - - #> - [CmdletBinding(DefaultParameterSetName = '__AllParameterSets')] - param( - # The account owner of the repository. The name is not case sensitive. - [Parameter(Mandatory)] - [string] $Owner, - - # The name of the repository without the .git extension. The name is not case sensitive. - [Parameter(Mandatory)] - [string] $Repository, - - # The number of results per page (max 100). - [Parameter(ParameterSetName = 'AllUsers')] - [ValidateRange(0, 100)] - [int] $PerPage, - - # 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" - Assert-GitHubContext -Context $Context -AuthType IAT, PAT, UAT - } - - process { - $body = @{ - per_page = $PerPage - } - - $inputObject = @{ - Method = 'GET' - APIEndpoint = "/repos/$Owner/$Repository/releases" - Body = $body - Context = $Context - } - - Invoke-GitHubAPI @inputObject | ForEach-Object { - Write-Output $_.Response - } - } - end { - Write-Debug "[$stackPath] - End" - } -} diff --git a/src/functions/public/API/Invoke-GitHubAPI.ps1 b/src/functions/public/API/Invoke-GitHubAPI.ps1 index 0cf5d8996..b18d5af1b 100644 --- a/src/functions/public/API/Invoke-GitHubAPI.ps1 +++ b/src/functions/public/API/Invoke-GitHubAPI.ps1 @@ -177,9 +177,9 @@ filter Invoke-GitHubAPI { if (-not $Body) { $Body = @{} } - $Body['per_page'] = Resolve-GitHubContextSetting -Name 'PerPage' -Value $PerPage -Context $Context - + $APICall.Uri = New-Uri -BaseUri $Uri -Query $Body -AsString + } elseif (($Method -eq 'POST') -and -not [string]::IsNullOrEmpty($UploadFilePath)) { $APICall.Uri = New-Uri -BaseUri $Uri -Query $Body -AsString } elseif ($Body) { if ($Body -is [hashtable]) { @@ -244,26 +244,11 @@ filter Invoke-GitHubAPI { 'application/.*json' { $results = $response.Content | ConvertFrom-Json } - 'text/plain' { - $results = $response.Content - } - 'text/html' { - $results = $response.Content - } 'application/octocat-stream' { [byte[]]$byteArray = $response.Content $results = [System.Text.Encoding]::UTF8.GetString($byteArray) } - 'zip' { - $results = $response.Content - } default { - if (-not $response.Content) { - $results = $null - break - } - Write-Warning "Unknown content type: $($headers.'Content-Type')" - Write-Warning 'Please report this issue!' $results = $response.Content } } @@ -279,6 +264,7 @@ filter Invoke-GitHubAPI { } while ($APICall['Uri']) } catch { $failure = $_ + $failure | Out-String -Stream | ForEach-Object { Write-Debug $_ } $headers = @{} foreach ($item in $failure.Exception.Response.Headers.GetEnumerator()) { $headers[$item.Key] = ($item.Value).Trim() -join ', ' diff --git a/src/functions/public/Releases/Assets/Add-GitHubReleaseAsset.ps1 b/src/functions/public/Releases/Assets/Add-GitHubReleaseAsset.ps1 index b52da2c5e..92123b57f 100644 --- a/src/functions/public/Releases/Assets/Add-GitHubReleaseAsset.ps1 +++ b/src/functions/public/Releases/Assets/Add-GitHubReleaseAsset.ps1 @@ -37,29 +37,42 @@ the old file before you can re-upload the new asset. .EXAMPLE - Add-GitHubReleaseAsset -Owner 'octocat' -Repository 'hello-world' -ID '7654321' -FilePath 'C:\Users\octocat\Downloads\hello-world.zip' + Add-GitHubReleaseAsset -Owner 'octocat' -Repository 'hello-world' -ID '7654321' -Path 'C:\Users\octocat\Downloads\hello-world.zip' Gets the release assets for the release with the ID '1234567' for the repository 'octocat/hello-world'. + .EXAMPLE + Add-GitHubReleaseAsset -Owner 'octocat' -Repository 'hello-world' -ID '7654321' -Path 'C:\Users\octocat\Projects\MyApp' + + Automatically creates a ZIP file from the contents of the MyApp directory and uploads it as a release asset. + + .INPUTS + GitHubRelease + + .OUTPUTS + GitHubReleaseAsset + + .LINK + https://psmodule.io/GitHub/Functions/Releases/Assets/Add-GitHubReleaseAsset/ + .NOTES [Upload a release asset](https://docs.github.com/rest/releases/assets#upload-a-release-asset) #> + [OutputType([GitHubReleaseAsset])] [CmdletBinding()] param( # The account owner of the repository. The name is not case sensitive. - [Parameter(Mandatory)] - [Alias('Organization')] - [Alias('User')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias('Organization', 'User')] [string] $Owner, # The name of the repository without the .git extension. The name is not case sensitive. - [Parameter(Mandatory)] + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $Repository, - # The unique identifier of the release. - [Parameter(Mandatory)] - [Alias('release_id')] - [string] $ID, + # The name of the tag to get a release from. + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [string] $Tag, #The name of the file asset. [Parameter()] @@ -69,14 +82,14 @@ [Parameter()] [string] $Label, - # The content type of the asset. - [Parameter()] - [string] $ContentType, - # The path to the asset file. [Parameter(Mandatory)] [alias('FullName')] - [string] $FilePath, + [string] $Path, + + # The 'Content-Type' for the payload. + [Parameter()] + [string] $ContentType, # 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. @@ -92,19 +105,36 @@ } process { - # If name is not provided, use the name of the file + try { + $item = Get-Item $Path + $isDirectory = $item.PSIsContainer + } catch { + throw "Error accessing the path: $_" + } + $fileToUpload = $Path + # If the path is a directory, create a zip file from the contents of the folder + if ($isDirectory) { + Write-Verbose 'Path is a directory. Zipping contents...' + $dirName = $item.Name + $TempFilePath = "$dirName.zip" + + Write-Verbose "Creating temporary zip file: $TempFilePath" + try { + Get-ChildItem -Path $Path | Compress-Archive -DestinationPath $TempFilePath -ErrorAction Stop -Force + $fileToUpload = $TempFilePath + } catch { + Remove-Item -Path $TempFilePath -Force -ErrorAction SilentlyContinue + } + } if (!$Name) { - $Name = (Get-Item $FilePath).Name + $Name = $item.Name } - - # If label is not provided, use the name of the file if (!$Label) { - $Label = (Get-Item $FilePath).Name + $Label = $item.Name } - # If content type is not provided, use the file extension if (!$ContentType) { - $ContentType = switch ((Get-Item $FilePath).Extension) { + $ContentType = switch ((Get-Item $fileToUpload).Extension) { '.zip' { 'application/zip' } '.tar' { 'application/x-tar' } '.gz' { 'application/gzip' } @@ -127,24 +157,42 @@ } } - $release = Get-GitHubRelease -Owner $Owner -Repository $Repository -ID $ID - $uploadURI = $release.upload_url -replace '{\?name,label}', "?name=$($Name)&label=$($Label)" + $release = Get-GitHubReleaseByTagName -Owner $Owner -Repository $Repository -Tag $Tag -Context $Context + + $body = @{ + name = $Name + label = $Label + } + $body | Remove-HashtableEntry -NullOrEmptyValues + + $urlParams = @{ + BaseUri = "https://uploads.$($Context.HostName)" + Path = "/repos/$Owner/$Repository/releases/$($release.id)/assets" + Query = $body + } + $uploadUrl = New-Uri @urlParams $inputObject = @{ Method = 'POST' - URI = $uploadURI + Uri = $uploadUrl ContentType = $ContentType - UploadFilePath = $FilePath + UploadFilePath = $fileToUpload + Context = $Context } Invoke-GitHubAPI @inputObject | ForEach-Object { - Write-Output $_.Response + [GitHubReleaseAsset]($_.Response) } } end { Write-Debug "[$stackPath] - End" } -} -#SkipTest:FunctionTest:Will add a test for this function in a future PR + clean { + if ($isDirectory) { + Write-Verbose "Cleaning up temporary zip file: $TempFilePath" + Remove-Item -Path $TempFilePath -Force -ErrorAction SilentlyContinue + } + } +} diff --git a/src/functions/public/Releases/Assets/Get-GitHubReleaseAsset.ps1 b/src/functions/public/Releases/Assets/Get-GitHubReleaseAsset.ps1 index 5106d462b..4de85f700 100644 --- a/src/functions/public/Releases/Assets/Get-GitHubReleaseAsset.ps1 +++ b/src/functions/public/Releases/Assets/Get-GitHubReleaseAsset.ps1 @@ -1,11 +1,13 @@ filter Get-GitHubReleaseAsset { <# .SYNOPSIS - List release assets based on a release ID or asset ID + List release assets based on a release ID, asset ID, or asset name .DESCRIPTION If an asset ID is provided, the asset is returned. If a release ID is provided, all assets for the release are returned. + If a release ID and name are provided, the specific named asset from that release is returned. + If a tag and name are provided, the specific named asset from the release with that tag is returned. .EXAMPLE Get-GitHubReleaseAsset -Owner 'octocat' -Repository 'hello-world' -ID '1234567' @@ -15,39 +17,63 @@ .EXAMPLE Get-GitHubReleaseAsset -Owner 'octocat' -Repository 'hello-world' -ReleaseID '7654321' - Gets the release assets for the release with the ID '7654321' for the repository 'octocat/hello-world'. + Gets all release assets for the release with the ID '7654321' for the repository 'octocat/hello-world'. - .NOTES - [Get a release asset](https://docs.github.com/rest/releases/assets#get-a-release-asset) + .EXAMPLE + Get-GitHubReleaseAsset -Owner 'octocat' -Repository 'hello-world' -ReleaseID '7654321' -Name 'example.zip' + + Gets the release asset named 'example.zip' from the release with ID '7654321' for the repository 'octocat/hello-world'. + + .EXAMPLE + Get-GitHubReleaseAsset -Owner 'octocat' -Repository 'hello-world' -Tag 'v1.0.0' -Name 'example.zip' + + Gets the release asset named 'example.zip' from the release tagged as 'v1.0.0' for the repository 'octocat/hello-world'. + + .INPUTS + GitHubRelease + + .OUTPUTS + GitHubReleaseAsset + + .LINK + https://psmodule.io/GitHub/Functions/Releases/Assets/Get-GitHubReleaseAsset #> - [CmdletBinding(DefaultParameterSetName = '__AllParameterSets')] + [OutputType([GitHubReleaseAsset])] + [CmdletBinding(DefaultParameterSetName = 'List assets from the latest release')] param( # The account owner of the repository. The name is not case sensitive. - [Parameter(Mandatory)] - [Alias('Organization')] - [Alias('User')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias('Organization', 'User')] [string] $Owner, # The name of the repository without the .git extension. The name is not case sensitive. - [Parameter(Mandatory)] + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $Repository, # The unique identifier of the asset. - [Parameter( - Mandatory, - ParameterSetName = 'ID' - )] - [Alias('asset_id')] + [Parameter(Mandatory, ParameterSetName = 'Get a specific asset by ID')] [string] $ID, # The unique identifier of the release. - [Parameter( - Mandatory, - ParameterSetName = 'ReleaseID' - )] - [Alias('release_id')] + [Parameter(Mandatory, ParameterSetName = 'List assets from a release by ID', ValueFromPipelineByPropertyName)] + [Alias('Release')] [string] $ReleaseID, + # The tag name of the release. + [Parameter(Mandatory, ParameterSetName = 'List assets from a release by tag')] + [string] $Tag, + + # The name of the asset to get. If specified, only assets with this name will be returned. + [Parameter()] + [string] $Name, + + # The number of results per page (max 100). + [Parameter(ParameterSetName = 'List assets from the latest release')] + [Parameter(ParameterSetName = 'List assets from a release by ID')] + [Parameter(ParameterSetName = 'List assets from a release by tag')] + [ValidateRange(0, 100)] + [int] $PerPage, + # 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()] @@ -62,12 +88,26 @@ } process { + $params = @{ + Owner = $Owner + Repository = $Repository + Context = $Context + Name = $Name + } + $params | Remove-HashtableEntry -NullOrEmptyValues + switch ($PSCmdlet.ParameterSetName) { - 'ReleaseID' { - Get-GitHubReleaseAssetByReleaseID -Owner $Owner -Repository $Repository -ReleaseID $ReleaseID -Context $Context + 'List assets from the latest release' { + Get-GitHubReleaseAssetFromLatest @params -PerPage $PerPage + } + 'List assets from a release by ID' { + Get-GitHubReleaseAssetByReleaseID @params -ID $ReleaseID -PerPage $PerPage } - 'ID' { - Get-GitHubReleaseAssetByID -Owner $Owner -Repository $Repository -ID $ID -Context $Context + 'List assets from a release by tag' { + Get-GitHubReleaseAssetByTag @params -Tag $Tag -PerPage $PerPage + } + 'Get a specific asset by ID' { + Get-GitHubReleaseAssetByID @params -ID $ID } } } @@ -76,5 +116,3 @@ Write-Debug "[$stackPath] - End" } } - -#SkipTest:FunctionTest:Will add a test for this function in a future PR diff --git a/src/functions/public/Releases/Assets/Remove-GitHubReleaseAsset.ps1 b/src/functions/public/Releases/Assets/Remove-GitHubReleaseAsset.ps1 index b6eaecfe8..1ebe7ab5b 100644 --- a/src/functions/public/Releases/Assets/Remove-GitHubReleaseAsset.ps1 +++ b/src/functions/public/Releases/Assets/Remove-GitHubReleaseAsset.ps1 @@ -11,6 +11,12 @@ Deletes the release asset with the ID '1234567' for the repository 'octocat/hello-world'. + .INPUTS + GitHubReleaseAsset + + .LINK + https://psmodule.io/GitHub/Functions/Releases/Assets/Remove-GitHubReleaseAsset + .NOTES [Delete a release asset](https://docs.github.com/rest/releases/assets#delete-a-release-asset) #> @@ -18,8 +24,7 @@ param( # The account owner of the repository. The name is not case sensitive. [Parameter(Mandatory)] - [Alias('Organization')] - [Alias('User')] + [Alias('Organization', 'User')] [string] $Owner, # The name of the repository without the .git extension. The name is not case sensitive. @@ -28,7 +33,6 @@ # The unique identifier of the asset. [Parameter(Mandatory)] - [Alias('asset_id')] [string] $ID, # The context to run the command in. Used to get the details for the API call. @@ -52,9 +56,7 @@ } if ($PSCmdlet.ShouldProcess("Asset with ID [$ID] in [$Owner/$Repository]", 'DELETE')) { - Invoke-GitHubAPI @inputObject | ForEach-Object { - Write-Output $_.Response - } + $null = Invoke-GitHubAPI @inputObject } } @@ -62,5 +64,3 @@ Write-Debug "[$stackPath] - End" } } - -#SkipTest:FunctionTest:Will add a test for this function in a future PR diff --git a/src/functions/public/Releases/Assets/Save-GitHubReleaseAsset.ps1 b/src/functions/public/Releases/Assets/Save-GitHubReleaseAsset.ps1 new file mode 100644 index 000000000..88321e97a --- /dev/null +++ b/src/functions/public/Releases/Assets/Save-GitHubReleaseAsset.ps1 @@ -0,0 +1,199 @@ +function Save-GitHubReleaseAsset { + <# + .SYNOPSIS + Downloads a GitHub Release asset. + + .DESCRIPTION + Downloads an asset from a repository release. The asset is downloaded as a file to the specified path + or the current directory by default. Users must have read access to the repository. For private repositories, + personal access tokens (classic) or OAuth tokens with the `repo` scope are required. + + .EXAMPLE + Save-GitHubReleaseAsset -Owner 'octocat' -Repository 'Hello-World' -ID '123456' -Path 'C:\Assets' + + Output: + ```powershell + Directory: C:\Assets + + Mode LastWriteTime Length Name + ---- ------------- ------ ---- + -a---- 03/31/2025 12:00 4194304 asset-123456.zip + ``` + + Downloads release asset ID '123456' from the 'Hello-World' repository owned by 'octocat' to the specified path. + + .EXAMPLE + Save-GitHubReleaseAsset -Owner 'octocat' -Repository 'Hello-World' -Tag 'v1.0.0' -Name 'binary.zip' -Path 'C:\Assets\app' -Expand -Force + + Output: + ```powershell + Directory: C:\Assets\app + + Mode LastWriteTime Length Name + ---- ------------- ------ ---- + -a---- 03/31/2025 12:00 5120 config.json + -a---- 03/31/2025 12:00 4194304 application.exe + ``` + + Downloads asset named 'binary.zip' from the release tagged as 'v1.0.0' in the 'Hello-World' repository owned by 'octocat' + to the specified path, overwriting existing files during download and extraction. + + .EXAMPLE + $params = @{ + Owner = 'octocat' + Repository = 'Hello-World' + ID = '123456' + Tag = 'v1.0.0' + Name = 'binary.zip' + } + Get-GitHubReleaseAsset @params | Save-GitHubReleaseAsset -Path 'C:\Assets' -Expand -Force + + Pipes a release asset object directly to the Save-GitHubReleaseAsset function, which downloads and extracts it. + + .INPUTS + GitHubReleaseAsset + + .OUTPUTS + System.IO.FileSystemInfo[] + + .NOTES + Contains the extracted file or folder information from the downloaded asset. + This output can include directories or files depending on the asset content. + + .LINK + https://psmodule.io/GitHub/Functions/Releases/Assets/Save-GitHubReleaseAsset/ + #> + [OutputType([System.IO.FileSystemInfo[]])] + [CmdletBinding(DefaultParameterSetName = 'By Asset ID')] + param( + # The account owner of the repository. The name is not case sensitive. + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'By Asset ID')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'By Release ID and Asset Name')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'By Tag and Asset Name')] + [string] $Owner, + + # The name of the repository without the .git extension. The name is not case sensitive. + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'By Asset ID')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'By Release ID and Asset Name')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'By Tag and Asset Name')] + [string] $Repository, + + # The unique identifier of the asset. + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'By Asset ID')] + [string] $ID, + + # The unique identifier of the release. + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'By Release ID and Asset Name')] + [string] $ReleaseID, + + # The tag name of the release. + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'By Tag and Asset Name')] + [string] $Tag, + + # The name of the asset to download. + [Parameter(Mandatory, ParameterSetName = 'By Release ID and Asset Name')] + [Parameter(Mandatory, ParameterSetName = 'By Tag and Asset Name')] + [string] $Name, + + # The GitHubReleaseAsset object containing the information about the asset to download. + [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'By Asset Object')] + [GitHubReleaseAsset] $ReleaseAssetObject, + + # Path to the file or folder for the download. Accepts relative or absolute paths. + [Parameter()] + [string] $Path = $PWD.Path, + + # When specified, the ZIP file is extracted to the same directory it was downloaded to. + [Parameter()] + [Alias('Extract')] + [switch] $Expand, + + # When specified, overwrites existing files during download and extraction. + [Parameter()] + [switch] $Force, + + # When specified, the downloaded file or the folder where the ZIP file was extracted to is returned. + [Parameter()] + [switch] $PassThru, + + # 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()] + [object] $Context = (Get-GitHubContext) + ) + + begin { + $stackPath = Get-PSCallStackPath + Write-Debug "[$stackPath] - Start" + $Context = Resolve-GitHubContext -Context $Context + Assert-GitHubContext -Context $Context -AuthType IAT, PAT, UAT + } + + process { + $asset = $null + + switch ($PSCmdlet.ParameterSetName) { + 'By Asset ID' { + $asset = Get-GitHubReleaseAsset -Owner $Owner -Repository $Repository -ID $ID -Context $Context + } + 'By Release ID and Asset Name' { + $asset = Get-GitHubReleaseAsset -Owner $Owner -Repository $Repository -ReleaseID $ReleaseID -Name $Name -Context $Context + } + 'By Tag and Asset Name' { + $asset = Get-GitHubReleaseAsset -Owner $Owner -Repository $Repository -Tag $Tag -Name $Name -Context $Context + } + 'By Asset Object' { + $asset = $ReleaseAssetObject + } + } + + if (-not $asset) { + throw 'Release asset not found. Please verify the parameters provided.' + } + + # Now download the asset + $inputObject = @{ + Method = 'GET' + Uri = $asset.Url + Context = $Context + } + + Invoke-GitHubAPI @inputObject | ForEach-Object { + $itemType = $Path.EndsWith('.zip') ? 'File' : 'Directory' + $isAbsolute = [System.IO.Path]::IsPathRooted($Path) + $filename = $asset.Name + + Write-Debug "Path: [$Path]" + Write-Debug "Type: [$itemType]" + Write-Debug "Is absolute: [$isAbsolute]" + Write-Debug "Filename: [$filename]" + + if ($itemType -eq 'Directory') { + $Path = Join-Path -Path $Path -ChildPath $filename + } + + $folderPath = [System.IO.Path]::GetDirectoryName($Path) + $folder = New-Item -Path $folderPath -ItemType Directory -Force + Write-Debug "Resolved final download path: [$Path]" + [System.IO.File]::WriteAllBytes($Path, $_.Response) + + # Check if we need to expand the downloaded file + if ($Expand -and $filename -match '\.(zip|tar|gz|bz2|xz|7z|rar)$') { + Write-Debug "Expanding asset to [$folder]" + Expand-Archive -LiteralPath $Path -DestinationPath $folder -Force:$Force + Write-Debug "Removing ZIP file [$Path]" + Remove-Item -LiteralPath $Path -Force + if ($PassThru) { + return $folder + } + } + if ($PassThru) { + return Get-Item -Path $Path + } + } + } + + end { + Write-Debug "[$stackPath] - End" + } +} diff --git a/src/functions/public/Releases/Assets/Set-GitHubReleaseAsset.ps1 b/src/functions/public/Releases/Assets/Update-GitHubReleaseAsset.ps1 similarity index 82% rename from src/functions/public/Releases/Assets/Set-GitHubReleaseAsset.ps1 rename to src/functions/public/Releases/Assets/Update-GitHubReleaseAsset.ps1 index a5def48de..2bfd9f554 100644 --- a/src/functions/public/Releases/Assets/Set-GitHubReleaseAsset.ps1 +++ b/src/functions/public/Releases/Assets/Update-GitHubReleaseAsset.ps1 @@ -1,4 +1,4 @@ -filter Set-GitHubReleaseAsset { +filter Update-GitHubReleaseAsset { <# .SYNOPSIS Update a release asset @@ -7,20 +7,29 @@ Users with push access to the repository can edit a release asset. .EXAMPLE - Set-GitHubReleaseAsset -Owner 'octocat' -Repository 'hello-world' -ID '1234567' -Name 'new_asset_name' -Label 'new_asset_label' + Update-GitHubReleaseAsset -Owner 'octocat' -Repository 'hello-world' -ID '1234567' -Name 'new_asset_name' -Label 'new_asset_label' Updates the release asset with the ID '1234567' for the repository 'octocat/hello-world' with the new name 'new_asset_name' and label 'new_asset_label'. + .INPUTS + GitHubReleaseAsset + + .OUTPUTS + GitHubReleaseAsset + + .LINK + https://psmodule.io/GitHub/Functions/Releases/Assets/Update-GitHubReleaseAsset + .NOTES [Update a release asset](https://docs.github.com/rest/releases/assets#update-a-release-asset) #> + [OutputType([GitHubReleaseAsset])] [CmdletBinding(SupportsShouldProcess)] param( # The account owner of the repository. The name is not case sensitive. [Parameter(Mandatory)] - [Alias('Organization')] - [Alias('User')] + [Alias('Organization', 'User')] [string] $Owner, # The name of the repository without the .git extension. The name is not case sensitive. @@ -29,7 +38,6 @@ # The unique identifier of the asset. [Parameter(Mandatory)] - [Alias('asset_id')] [string] $ID, #The name of the file asset. @@ -75,7 +83,7 @@ if ($PSCmdlet.ShouldProcess("assets for release with ID [$ID] in [$Owner/$Repository]", 'Set')) { Invoke-GitHubAPI @inputObject | ForEach-Object { - Write-Output $_.Response + [GitHubReleaseAsset]($_.Response) } } } @@ -84,5 +92,3 @@ Write-Debug "[$stackPath] - End" } } - -#SkipTest:FunctionTest:Will add a test for this function in a future PR diff --git a/src/functions/public/Releases/Releases/Get-GitHubRelease.ps1 b/src/functions/public/Releases/Get-GitHubRelease.ps1 similarity index 55% rename from src/functions/public/Releases/Releases/Get-GitHubRelease.ps1 rename to src/functions/public/Releases/Get-GitHubRelease.ps1 index ede7522d6..a52b1a424 100644 --- a/src/functions/public/Releases/Releases/Get-GitHubRelease.ps1 +++ b/src/functions/public/Releases/Get-GitHubRelease.ps1 @@ -1,7 +1,7 @@ filter Get-GitHubRelease { <# .SYNOPSIS - List releases + Retrieves GitHub release information for a repository. .DESCRIPTION This returns a list of releases, which does not include regular Git tags that have not been associated with a release. @@ -11,12 +11,12 @@ .EXAMPLE Get-GitHubRelease -Owner 'octocat' -Repository 'hello-world' - Gets the releases for the repository 'hello-world' owned by 'octocat'. + Gets the latest release for the repository 'hello-world' owned by 'octocat'. .EXAMPLE - Get-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -Latest + Get-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -AllVersions - Gets the latest releases for the repository 'hello-world' owned by 'octocat'. + Gets all releases for the repository 'hello-world' owned by 'octocat'. .EXAMPLE Get-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -Tag 'v1.0.0' @@ -28,51 +28,48 @@ Gets the release with the ID '1234567' for the repository 'hello-world' owned by 'octocat'. - .NOTES - [List releases](https://docs.github.com/rest/releases/releases#list-releases) - [Get the latest release](https://docs.github.com/rest/releases/releases#get-the-latest-release) + .INPUTS + GitHubRepository + + .OUTPUTS + GitHubRelease + + .LINK + https://psmodule.io/GitHub/Functions/Releases/Get-GitHubRelease/ #> - [CmdletBinding(DefaultParameterSetName = 'All')] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Latest', Justification = 'Required for parameter set')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', 'AllVersions', + Justification = 'Using the ParameterSetName to determine the context of the command.' + )] + [OutputType([GitHubRelease])] + [CmdletBinding(DefaultParameterSetName = 'Latest')] param( # The account owner of the repository. The name is not case sensitive. - [Parameter(Mandatory)] - [Alias('Organization')] - [Alias('User')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias('Organization', 'User')] [string] $Owner, # The name of the repository without the .git extension. The name is not case sensitive. - [Parameter(Mandatory)] + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $Repository, - # The number of results per page (max 100). - [Parameter(ParameterSetName = 'All')] - [ValidateRange(0, 100)] - [int] $PerPage, - - # Get the latest release only - [Parameter( - Mandatory, - ParameterSetName = 'Latest' - )] - [switch] $Latest, + # Get all releases instead of just the latest. + [Parameter(Mandatory, ParameterSetName = 'AllVersions')] + [switch] $AllVersions, # The name of the tag to get a release from. - [Parameter( - Mandatory, - ParameterSetName = 'Tag' - )] - [Alias('tag_name')] + [Parameter(Mandatory, ParameterSetName = 'Tag')] [string] $Tag, # The unique identifier of the release. - [Parameter( - Mandatory, - ParameterSetName = 'ID' - )] - [Alias('release_id')] + [Parameter(Mandatory, ParameterSetName = 'ID')] [string] $ID, + # The number of results per page (max 100). + [Parameter(ParameterSetName = 'AllVersions')] + [ValidateRange(0, 100)] + [int] $PerPage, + # 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()] @@ -87,26 +84,34 @@ } process { + $params = @{ + Owner = $Owner + Repository = $Repository + Context = $Context + } + Write-Debug "ParameterSet: $($PSCmdlet.ParameterSetName)" switch ($PSCmdlet.ParameterSetName) { - 'All' { - Get-GitHubReleaseAll -Owner $Owner -Repository $Repository -PerPage $PerPage -Context $Context - } - 'Latest' { - Get-GitHubReleaseLatest -Owner $Owner -Repository $Repository -Context $Context + 'AllVersions' { + Get-GitHubReleaseAll @params -PerPage $PerPage } 'Tag' { - Get-GitHubReleaseByTagName -Owner $Owner -Repository $Repository -Tag $Tag -Context $Context + $release = Get-GitHubReleaseByTagName @params -Tag $Tag + if ($release) { + $release + } else { + Get-GithubReleaseAll @params -PerPage $PerPage | Where-Object { $_.Tag -eq $Tag } + } } 'ID' { - Get-GitHubReleaseByID -Owner $Owner -Repository $Repository -ID $ID -Context $Context + Get-GitHubReleaseByID @params -ID $ID + } + 'Latest' { + Get-GitHubReleaseLatest @params } } - } end { Write-Debug "[$stackPath] - End" } } - -#SkipTest:FunctionTest:Will add a test for this function in a future PR diff --git a/src/functions/public/Releases/Releases/New-GitHubRelease.ps1 b/src/functions/public/Releases/New-GitHubRelease.ps1 similarity index 54% rename from src/functions/public/Releases/Releases/New-GitHubRelease.ps1 rename to src/functions/public/Releases/New-GitHubRelease.ps1 index 021a3900a..488560be0 100644 --- a/src/functions/public/Releases/Releases/New-GitHubRelease.ps1 +++ b/src/functions/public/Releases/New-GitHubRelease.ps1 @@ -11,38 +11,65 @@ and "[Dealing with secondary rate limits](https://docs.github.com/rest/guides/best-practices-for-integrators#dealing-with-secondary-rate-limits)" for details. .EXAMPLE - New-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -TagName 'v1.0.0' -TargetCommitish 'main' -Body 'Release notes' + New-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -Tag 'v1.0.0' -Target 'main' -Notes 'Release notes' - Creates a release for the repository 'octocat/hello-world' with the tag 'v1.0.0' and the target commitish 'main'. + Creates a release for the repository 'octocat/hello-world' on the 'main' branch with the tag 'v1.0.0'. + + .EXAMPLE + New-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -Tag 'v0.9.0' -Name 'Beta Release' -Draft -Prerelease + + Creates a draft prerelease for the repository 'octocat/hello-world' with the tag 'v0.9.0' using the default target branch ('main'). + + .EXAMPLE + New-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -Tag 'v2.0.0' -Latest + + Creates a release for the repository 'octocat/hello-world' with the tag 'v2.0.0' and marks it as the latest release. + Note that when using -Latest, you cannot use -Draft or -Prerelease as they are mutually exclusive. + + .EXAMPLE + New-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -Tag 'v1.1.0' -GenerateReleaseNotes + + Creates a release for the repository 'octocat/hello-world' with the tag 'v1.1.0' and automatically generates release notes based on commits since the previous release. + + .EXAMPLE + New-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -Tag 'v1.2.0' -DiscussionCategoryName 'Announcements' -Notes 'Major update with new features' + + Creates a release for the repository 'octocat/hello-world' with the tag 'v1.2.0' and creates a discussion in the 'Announcements' category linked to this release. + + .INPUTS + GitHubRepository + + .OUTPUTS + GitHubRelease + + .LINK + https://psmodule.io/GitHub/Functions/Releases/New-GitHubRelease/ .NOTES [Create a release](https://docs.github.com/rest/releases/releases#create-a-release) #> - [OutputType([pscustomobject])] + [OutputType([GitHubRelease])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidLongLines', '', Justification = 'Contains a long link.')] - [CmdletBinding(SupportsShouldProcess)] + [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Not latest')] param( # The account owner of the repository. The name is not case sensitive. - [Parameter(Mandatory)] - [Alias('Organization')] - [Alias('User')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias('Organization', 'User')] [string] $Owner, # The name of the repository without the .git extension. The name is not case sensitive. - [Parameter(Mandatory)] + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $Repository, # The name of the tag. [Parameter(Mandatory)] - [Alias('tag_name')] - [string] $TagName, + [string] $Tag, - # Specifies the commitish value that determines where the Git tag is created from. + # Specifies the reference value that determines where the Git tag is created from. # Can be any branch or commit SHA. Unused if the Git tag already exists. # API Default: the repository's default branch. [Parameter()] - [Alias('target_commitish')] - [string] $TargetCommitish = 'main', + [string] $Target, # The name of the release. [Parameter()] @@ -50,33 +77,33 @@ # Text describing the contents of the tag. [Parameter()] - [string] $Body, + [string] $Notes, # Whether the release is a draft. - [Parameter()] + [Parameter(ParameterSetName = 'Not latest')] [switch] $Draft, # Whether to identify the release as a prerelease. - [Parameter()] + [Parameter(ParameterSetName = 'Not latest')] [switch] $Prerelease, # If specified, a discussion of the specified category is created and linked to the release. # The value must be a category that already exists in the repository. # For more information, see [Managing categories for discussions in your repository](https://docs.github.com/discussions/managing-discussions-for-your-community/managing-categories-for-discussions-in-your-repository). [Parameter()] - [Alias('discussion_category_name')] [string] $DiscussionCategoryName, - # Whether to automatically generate the name and body for this release. If name is specified, the specified name will be used; otherwise,a name will be automatically generated. If body is specified, the body will be pre-pended to the automatically generated notes. + # Whether to automatically generate the name and body for this release. If name is specified, the specified name will be used; otherwise, + # a name will be automatically generated. If body is specified, the body will be pre-pended to the automatically generated notes. [Parameter()] - [Alias('generate_release_notes')] [switch] $GenerateReleaseNotes, - # Specifies whether this release should be set as the latest release for the repository. Drafts and prereleases cannot be set as latest. Defaults to true for newly published releases. legacy specifies that the latest release should be determined based on the release creation date and higher semantic version. - [Parameter()] - [Alias('make_latest')] - [ValidateSet('true', 'false', 'legacy')] - [string] $MakeLatest = 'true', + # Specifies whether this release should be set as the latest release for the repository. Drafts and prereleases cannot be set as latest. + # If not specified the latest release is determined based on the release creation date and higher semantic version. + # If set to true, the release will be set as the latest release for the repository. + # If set to false, the release will not be set as the latest release for the repository. + [Parameter(ParameterSetName = 'Set latest')] + [switch] $Latest, # 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. @@ -93,15 +120,15 @@ process { $body = @{ - tag_name = $TagName - target_commitish = $TargetCommitish + tag_name = $Tag + target_commitish = $Target name = $Name - body = $Body + body = $Notes discussion_category_name = $DiscussionCategoryName - make_latest = $MakeLatest - generate_release_notes = $GenerateReleaseNotes - draft = $Draft - prerelease = $Prerelease + generate_release_notes = [bool]$GenerateReleaseNotes + make_latest = ([bool]$Latest).ToString().ToLower() + draft = [bool]$Draft + prerelease = [bool]$Prerelease } $body | Remove-HashtableEntry -NullOrEmptyValues @@ -114,7 +141,7 @@ if ($PSCmdlet.ShouldProcess("$Owner/$Repository", 'Create a release')) { Invoke-GitHubAPI @inputObject | ForEach-Object { - Write-Output $_.Response + [GitHubRelease]::new($_.Response , $Owner, $Repository, $Latest) } } } @@ -123,5 +150,3 @@ Write-Debug "[$stackPath] - End" } } - -#SkipTest:FunctionTest:Will add a test for this function in a future PR diff --git a/src/functions/public/Releases/Releases/New-GitHubReleaseNote.ps1 b/src/functions/public/Releases/New-GitHubReleaseNote.ps1 similarity index 67% rename from src/functions/public/Releases/Releases/New-GitHubReleaseNote.ps1 rename to src/functions/public/Releases/New-GitHubReleaseNote.ps1 index 849b1ed34..299a185b0 100644 --- a/src/functions/public/Releases/Releases/New-GitHubReleaseNote.ps1 +++ b/src/functions/public/Releases/New-GitHubReleaseNote.ps1 @@ -1,19 +1,18 @@ filter New-GitHubReleaseNote { <# .SYNOPSIS - List releases + Generate release notes content for a release. .DESCRIPTION - Generate a name and body describing a [release](https://docs.github.com/rest/releases/releases#get-a-release). - The body content will be Markdown formatted and contain information like - the changes since last release and users who contributed. The generated release notes are not saved anywhere. They are - intended to be generated and used when creating a new release. + Generate a name and body describing a [release](https://docs.github.com/rest/releases/releases#generate-release-notes-content-for-a-release). + The body content will be Markdown formatted and contain information like the changes since last release and users who contributed. + The generated release notes are not saved anywhere. They are intended to be generated and used when creating a new release. .EXAMPLE $params = @{ - Owner = 'octocat' - Repo = 'hello-world' - TagName = 'v1.0.0' + Owner = 'octocat' + Repository = 'hello-world' + Tag = 'v1.0.0' } New-GitHubReleaseNote @params @@ -23,10 +22,10 @@ .EXAMPLE $params = @{ - Owner = 'octocat' - Repo = 'hello-world' - TagName = 'v1.0.0' - TargetCommitish = 'main' + Owner = 'octocat' + Repository = 'hello-world' + Tag = 'v1.0.0' + Target = 'main' } New-GitHubReleaseNote @params @@ -35,11 +34,11 @@ .EXAMPLE $params = @{ - Owner = 'octocat' - Repo = 'hello-world' - TagName = 'v1.0.0' - TargetCommitish = 'main' - PreviousTagName = 'v0.9.2' + Owner = 'octocat' + Repository = 'hello-world' + Tag = 'v1.0.0' + Target = 'main' + PreviousTag = 'v0.9.2' ConfigurationFilePath = '.github/custom_release_config.yml' } New-GitHubReleaseNote @params @@ -48,8 +47,19 @@ The release notes will be based on the changes between the tags 'v0.9.2' and 'v1.0.0' and generated based on the configuration file located in the repository at '.github/custom_release_config.yml'. + .OUTPUTS + pscustomobject + .NOTES - [Generate release notes content for a release](https://docs.github.com/rest/releases/releases#list-releases) + The returned object contains the following properties: + - Name: The name of the release. + - Notes: The body of the release notes. + + .LINK + https://psmodule.io/GitHub/Functions/Releases/New-GitHubReleaseNote/ + + .LINK + [Generate release notes content for a release](https://docs.github.com/rest/releases/releases#generate-release-notes-content-for-a-release) #> [OutputType([pscustomobject])] [Alias('Generate-GitHubReleaseNotes')] @@ -57,39 +67,33 @@ [CmdletBinding(SupportsShouldProcess)] param( # The account owner of the repository. The name is not case sensitive. - [Parameter(Mandatory)] - [Alias('Organization')] - [Alias('User')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias('Organization', 'User')] [string] $Owner, # The name of the repository without the .git extension. The name is not case sensitive. - [Parameter(Mandatory)] + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $Repository, # The tag name for the release. This can be an existing tag or a new one. - [Parameter(Mandatory)] - [Alias('tag_name')] - [string] $TagName, + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [string] $Tag, # Specifies the commitish value that will be the target for the release's tag. # Required if the supplied tag_name does not reference an existing tag. # Ignored if the tag_name already exists. [Parameter()] - [Alias('target_commitish')] - [string] $TargetCommitish, + [string] $Target, # The name of the previous tag to use as the starting point for the release notes. # Use to manually specify the range for the set of changes considered as part this release. [Parameter()] - [Alias('previous_tag_name')] - [string] $PreviousTagName, - + [string] $PreviousTag, # Specifies a path to a file in the repository containing configuration settings used for generating the release notes. # If unspecified, the configuration file located in the repository at '.github/release.yml' or '.github/release.yaml' will be used. # If that is not present, the default configuration will be used. [Parameter()] - [Alias('configuration_file_path')] [string] $ConfigurationFilePath, # The context to run the command in. Used to get the details for the API call. @@ -107,9 +111,9 @@ process { $body = @{ - tag_name = $TagName - target_commitish = $TargetCommitish - previous_tag_name = $PreviousTagName + tag_name = $Tag + target_commitish = $Target + previous_tag_name = $PreviousTag configuration_file_path = $ConfigurationFilePath } $body | Remove-HashtableEntry -NullOrEmptyValues @@ -118,11 +122,15 @@ Method = 'POST' APIEndpoint = "/repos/$Owner/$Repository/releases/generate-notes" Body = $body + Context = $Context } - if ($PSCmdlet.ShouldProcess("$Owner/$Repository", 'Create release notes')) { + if ($PSCmdlet.ShouldProcess("release notes for release on $Owner/$Repository", 'Create')) { Invoke-GitHubAPI @inputObject | ForEach-Object { - Write-Output $_.Response + [PSCustomObject]@{ + Name = $_.Response.name + Notes = $_.Response.body + } } } } @@ -131,5 +139,3 @@ Write-Debug "[$stackPath] - End" } } - -#SkipTest:FunctionTest:Will add a test for this function in a future PR diff --git a/src/functions/public/Releases/Releases/Set-GitHubRelease.ps1 b/src/functions/public/Releases/Releases/Set-GitHubRelease.ps1 deleted file mode 100644 index 1559ef234..000000000 --- a/src/functions/public/Releases/Releases/Set-GitHubRelease.ps1 +++ /dev/null @@ -1,124 +0,0 @@ -filter Set-GitHubRelease { - <# - .SYNOPSIS - Update a release - - .DESCRIPTION - Users with push access to the repository can edit a release. - - .EXAMPLE - Set-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -ID '1234567' -Body 'Release notes' - - Updates the release with the ID '1234567' for the repository 'octocat/hello-world' with the body 'Release notes'. - - .NOTES - [Update a release](https://docs.github.com/rest/releases/releases#update-a-release) - #> - [OutputType([pscustomobject])] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidLongLines', '', Justification = 'Contains a long link.')] - [CmdletBinding(SupportsShouldProcess)] - param( - # The account owner of the repository. The name is not case sensitive. - [Parameter(Mandatory)] - [Alias('Organization')] - [Alias('User')] - [string] $Owner, - - # The name of the repository without the .git extension. The name is not case sensitive. - [Parameter(Mandatory)] - [string] $Repository, - - # The unique identifier of the release. - [Parameter(Mandatory)] - [Alias('release_id')] - [string] $ID, - - # The name of the tag. - [Parameter()] - [Alias('tag_name')] - [string] $TagName, - - # Specifies the commitish value that determines where the Git tag is created from. - # Can be any branch or commit SHA. Unused if the Git tag already exists. - # API Default: the repository's default branch. - [Parameter()] - [Alias('target_commitish')] - [string] $TargetCommitish, - - # The name of the release. - [Parameter()] - [string] $Name, - - # Text describing the contents of the tag. - [Parameter()] - [string] $Body, - - # Whether the release is a draft. - [Parameter()] - [switch] $Draft, - - # Whether to identify the release as a prerelease. - [Parameter()] - [switch] $Prerelease, - - # If specified, a discussion of the specified category is created and linked to the release. - # The value must be a category that already exists in the repository. - # For more information, see [Managing categories for discussions in your repository](https://docs.github.com/discussions/managing-discussions-for-your-community/managing-categories-for-discussions-in-your-repository). - [Parameter()] - [Alias('discussion_category_name')] - [string] $DiscussionCategoryName, - - # Specifies whether this release should be set as the latest release for the repository. Drafts and prereleases cannot be set as latest. - # Defaults to true for newly published releases. legacy specifies that the latest release should be determined based on the release creation - # date and higher semantic version. - [Parameter()] - [Alias('make_latest')] - [ValidateSet('true', 'false', 'legacy')] - [string] $MakeLatest = 'true', - - # 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()] - [object] $Context = (Get-GitHubContext) - ) - - begin { - $stackPath = Get-PSCallStackPath - Write-Debug "[$stackPath] - Start" - $Context = Resolve-GitHubContext -Context $Context - Assert-GitHubContext -Context $Context -AuthType IAT, PAT, UAT - } - - process { - $body = @{ - tag_name = $TagName - target_commitish = $TargetCommitish - name = $Name - body = $Body - discussion_category_name = $DiscussionCategoryName - make_latest = $MakeLatest - draft = $Draft - prerelease = $Prerelease - } - $body | Remove-HashtableEntry -NullOrEmptyValues - - $inputObject = @{ - Method = 'PATCH' - APIEndpoint = "/repos/$Owner/$Repository/releases/$ID" - Body = $body - Context = $Context - } - - if ($PSCmdlet.ShouldProcess("release with ID [$ID] in [$Owner/$Repository]", 'Update')) { - Invoke-GitHubAPI @inputObject | ForEach-Object { - Write-Output $_.Response - } - } - } - - end { - Write-Debug "[$stackPath] - End" - } -} - -#SkipTest:FunctionTest:Will add a test for this function in a future PR diff --git a/src/functions/public/Releases/Releases/Remove-GitHubRelease.ps1 b/src/functions/public/Releases/Remove-GitHubRelease.ps1 similarity index 75% rename from src/functions/public/Releases/Releases/Remove-GitHubRelease.ps1 rename to src/functions/public/Releases/Remove-GitHubRelease.ps1 index 1184c427e..991aa2c53 100644 --- a/src/functions/public/Releases/Releases/Remove-GitHubRelease.ps1 +++ b/src/functions/public/Releases/Remove-GitHubRelease.ps1 @@ -11,27 +11,32 @@ Deletes the release with the ID '1234567' for the repository 'octocat/hello-world'. - .NOTES + .INPUTS + GitHubRelease + + .OUTPUTS + None + + .LINK + https://psmodule.io/GitHub/Functions/Releases/Get-GitHubRelease/ + + .LINK [Delete a release](https://docs.github.com/rest/releases/releases#delete-a-release) #> - [OutputType([pscustomobject])] + [OutputType([void])] [CmdletBinding(SupportsShouldProcess)] param( # The account owner of the repository. The name is not case sensitive. - [Parameter(Mandatory)] - [Alias('Organization')] - [Alias('User')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias('Organization', 'User')] [string] $Owner, # The name of the repository without the .git extension. The name is not case sensitive. - [Parameter(Mandatory)] + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $Repository, # The unique identifier of the release. - [Parameter( - Mandatory - )] - [Alias('release_id')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ID, # The context to run the command in. Used to get the details for the API call. @@ -54,10 +59,8 @@ Context = $Context } - if ($PSCmdlet.ShouldProcess("Release with ID [$ID] in [$Owner/$Repository]", 'DELETE')) { - Invoke-GitHubAPI @inputObject | ForEach-Object { - Write-Output $_.Response - } + if ($PSCmdlet.ShouldProcess("Release with ID [$ID] in [$Owner/$Repository]", 'Delete')) { + $null = Invoke-GitHubAPI @inputObject } } @@ -65,5 +68,3 @@ Write-Debug "[$stackPath] - End" } } - -#SkipTest:FunctionTest:Will add a test for this function in a future PR diff --git a/src/functions/public/Releases/Set-GitHubRelease.ps1 b/src/functions/public/Releases/Set-GitHubRelease.ps1 new file mode 100644 index 000000000..e0df36825 --- /dev/null +++ b/src/functions/public/Releases/Set-GitHubRelease.ps1 @@ -0,0 +1,172 @@ +filter Set-GitHubRelease { + <# + .SYNOPSIS + Creates or updates a release. + + .DESCRIPTION + The Set-GitHubRelease cmdlet creates a new GitHub release or updates an existing one for a specified tag. + + This function first checks if a release with the specified tag already exists: + - If the release exists, it will update the existing release with the provided parameters + - If the release doesn't exist, it will create a new release + + You can specify whether the release is a draft or prerelease, generate release notes automatically, + link a discussion to the release, and set a release as the latest for the repository. + + When using the 'Latest' parameter, the release will be promoted from draft/prerelease status to a full release. + + .EXAMPLE + Set-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -Tag 'v1.0.0' -Target 'main' -Notes 'Release notes' + + Creates a new release with tag 'v1.0.0' targeting the 'main' branch. + + .EXAMPLE + Set-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -Tag 'v1.0.0' -Notes 'Updated release notes' + + Updates an existing release with tag 'v1.0.0' to have new release notes. + + .EXAMPLE + Set-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -Tag 'v1.0.0' -Draft + + Creates or updates a release as a draft release. + + .EXAMPLE + Set-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -Tag 'v1.0.0' -Prerelease + + Creates or updates a release as a prerelease. + + .EXAMPLE + Set-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -Tag 'v1.0.0' -Latest + + Sets the release with tag 'v1.0.0' as the latest release for the repository. If the release was a draft or prerelease, + it will be promoted to a full release. + + .EXAMPLE + Set-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -Tag 'v1.0.0' -GenerateReleaseNotes + + Creates or updates a release with automatically generated release notes based on pull requests and commits. + + .EXAMPLE + Get-GitHubRepository -Owner 'octocat' -Repository 'hello-world' | Set-GitHubRelease -Tag 'v1.0.0' -Notes 'Release notes' + + Creates or updates a release using pipeline input for the repository. + + .INPUTS + GitHubRepository + + .OUTPUTS + GitHubRelease + + .LINK + https://psmodule.io/GitHub/Functions/Releases/Set-GitHubRelease/ + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSShouldProcess', '', Scope = 'Function', + Justification = 'This check is performed in the private functions.' + )] + [OutputType([GitHubRelease])] + [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Not latest')] + param( + # The account owner of the repository. The name is not case sensitive. + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias('Organization', 'User')] + [string] $Owner, + + # The name of the repository without the .git extension. The name is not case sensitive. + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [string] $Repository, + + # The name of the tag. + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [string] $Tag, + + # Specifies the reference value that determines where the Git tag is created from. + # Can be any branch or commit SHA. Unused if the Git tag already exists. + # API Default: the repository's default branch. + [Parameter()] + [string] $Target = 'main', + + # The name of the release. + [Parameter()] + [string] $Name, + + # Text describing the contents of the tag. + [Parameter()] + [string] $Notes, + + # Whether the release is a draft. + [Parameter(ParameterSetName = 'Not latest')] + [switch] $Draft, + + # Whether to identify the release as a prerelease. + [Parameter(ParameterSetName = 'Not latest')] + [switch] $Prerelease, + + # If specified, a discussion of the specified category is created and linked to the release. + # The value must be a category that already exists in the repository. + [Parameter()] + [string] $DiscussionCategoryName, + + # Whether to automatically generate the name and body for this release. If name is specified, the specified name will be used; otherwise, + # a name will be automatically generated. If body is specified, the body will be pre-pended to the automatically generated notes. + [Parameter()] + [switch] $GenerateReleaseNotes, + + # Specifies whether this release should be set as the latest release for the repository. If the release is a draft or a prerelease, setting + # this parameters will promote the release to a release, setting the draft and prerelease parameters to false. + [Parameter(Mandatory, ParameterSetName = 'Set latest')] + [switch] $Latest, + + # 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()] + [object] $Context = (Get-GitHubContext) + ) + + begin { + $stackPath = Get-PSCallStackPath + Write-Debug "[$stackPath] - Start" + $Context = Resolve-GitHubContext -Context $Context + Assert-GitHubContext -Context $Context -AuthType IAT, PAT, UAT + } + + process { + $scope = @{ + Owner = $Owner + Repository = $Repository + Context = $Context + } + + $params = @{ + Tag = $Tag + Target = $Target + Name = $Name + Notes = $Notes + GenerateReleaseNotes = [bool]$GenerateReleaseNotes + DiscussionCategoryName = $DiscussionCategoryName + } + + switch ($PSCmdlet.ParameterSetName) { + 'Set latest' { + $params['Latest'] = [bool]$Latest + } + 'Not latest' { + $params['Draft'] = [bool]$Draft + $params['Prerelease'] = [bool]$Prerelease + } + } + + $release = Get-GitHubRelease @scope -Tag $Tag + if ($release) { + $ID = $release.ID + $params['ID'] = $ID + Update-GitHubRelease @scope @params -Declare + } else { + New-GitHubRelease @scope @params + } + } + + end { + Write-Debug "[$stackPath] - End" + } +} diff --git a/src/functions/public/Releases/Update-GitHubRelease.ps1 b/src/functions/public/Releases/Update-GitHubRelease.ps1 new file mode 100644 index 000000000..6a9bec95d --- /dev/null +++ b/src/functions/public/Releases/Update-GitHubRelease.ps1 @@ -0,0 +1,196 @@ +filter Update-GitHubRelease { + <# + .SYNOPSIS + Update a release + + .DESCRIPTION + Users with push access to the repository can edit a release. + + You must specify either the ID or Tag parameter to identify the release to update. + The function also accepts GitHubRelease objects through the pipeline. + + .EXAMPLE + Update-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -ID '1234567' -Notes 'Release notes' + + Updates the release with the ID '1234567' for the repository 'octocat/hello-world' with the note 'Release notes'. + + .EXAMPLE + Update-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -Tag 'v1.0' -Name 'Release v1.0' -Notes 'Stable release' + + Updates the release with the tag 'v1.0' for the repository 'octocat/hello-world' with a new name and notes. + + .EXAMPLE + Get-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -Tag 'v1.0' | + Update-GitHubRelease -Draft:$false -Prerelease + + Gets a release by tag and updates it to be a prerelease (not a draft). + + .EXAMPLE + Update-GitHubRelease -Owner 'octocat' -Repository 'hello-world' -Tag 'v1.0' -Latest -GenerateReleaseNotes + + Updates the release with tag 'v1.0' to be the latest release and automatically generates release notes. + + .LINK + https://psmodule.io/GitHub/Functions/Releases/Update-GitHubRelease + + .INPUTS + GitHubRelease + + .OUTPUTS + GitHubRelease + + .NOTES + [Update a release](https://docs.github.com/rest/releases/releases#update-a-release) + #> + [OutputType([GitHubRelease])] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidLongLines', '', Justification = 'Contains a long link.')] + [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Not latest')] + param( + # The account owner of the repository. The name is not case sensitive. + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias('Organization', 'User')] + [string] $Owner, + + # The name of the repository without the .git extension. The name is not case sensitive. + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [string] $Repository, + + # The unique identifier of the release. + [Parameter()] + [string] $ID, + + # The name of the tag. + [Parameter()] + [string] $Tag, + + # Specifies the commitish value that determines where the Git tag is created from. + # Can be any branch or commit SHA. Unused if the Git tag already exists. + # API Default: the repository's default branch. + [Parameter()] + [string] $Target, + + # The name of the release. + [Parameter()] + [string] $Name, + + # Text describing the contents of the tag. + [Parameter()] + [string] $Notes, + + # Whether the release is a draft. + [Parameter(ParameterSetName = 'Not latest')] + [System.Nullable[switch]] $Draft, + + # Whether to identify the release as a prerelease. + [Parameter(ParameterSetName = 'Not latest')] + [System.Nullable[switch]] $Prerelease, + + # If specified, a discussion of the specified category is created and linked to the release. + # The value must be a category that already exists in the repository. + # For more information, see [Managing categories for discussions in your repository](https://docs.github.com/discussions/managing-discussions-for-your-community/managing-categories-for-discussions-in-your-repository). + [Parameter()] + [string] $DiscussionCategoryName, + + # Whether to automatically generate the name and body for this release. If name is specified, the specified name will be used; otherwise, + # a name will be automatically generated. If body is specified, the body will be pre-pended to the automatically generated notes. + [Parameter()] + [switch] $GenerateReleaseNotes, + + # Specifies whether this release should be set as the latest release for the repository. If the release is a draft or a prerelease, setting + # this parameters will promote the release to a release, setting the draft and prerelease parameters to false. + [Parameter(Mandatory, ParameterSetName = 'Set latest')] + [System.Nullable[switch]] $Latest, + + # Takes all parameters and updates the release with the provided _AND_ the default values of the non-provided parameters. + # Used for Set-GitHubRelease. + [Parameter()] + [switch] $Declare, + + # The release to update + [Parameter(ValueFromPipeline)] + [GitHubRelease] $ReleaseObject, + + # 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()] + [object] $Context = (Get-GitHubContext) + ) + + begin { + $stackPath = Get-PSCallStackPath + Write-Debug "[$stackPath] - Start" + $Context = Resolve-GitHubContext -Context $Context + Assert-GitHubContext -Context $Context -AuthType IAT, PAT, UAT + } + + process { + $ID = $ReleaseObject.ID + + if (-not $ID -and -not $Tag) { + throw 'You must specify either the ID or the Tag parameter.' + } + + if ($GenerateReleaseNotes) { + $generated = New-GitHubReleaseNote -Owner $Owner -Repository $Repository -Tag $Tag -Context $Context + $Name = -not [string]::IsNullOrEmpty($Name) ? $Name : $generated.Name + $Notes = -not [string]::IsNullOrEmpty($Notes) ? $Notes, $generated.Notes -join "`n" : $generated.Notes + } + + $body = @{ + tag_name = $Tag + target_commitish = $Target + name = $Name + body = $Notes + } + + if ([string]::IsNullOrEmpty($ID) -and -not [string]::IsNullOrEmpty($Tag)) { + $release = if ($ReleaseObject) { + $ReleaseObject + } else { + Get-GitHubRelease -Owner $Owner -Repository $Repository -Tag $Tag -Context $Context + } + if (-not $release) { + throw "Release with tag [$Tag] not found in [$Owner/$Repository]." + } + $ID = $release.ID + $body.Remove('tag_name') + } + + $repo = Get-GitHubRepositoryByName -Owner $Owner -Name $Repository -Context $Context + if ($repo.HasDiscussions) { + $body['discussion_category_name'] = $DiscussionCategoryName + } + if (-not $Declare) { + $body | Remove-HashtableEntry -NullOrEmptyValues + } + + if ($Latest) { + $body['make_latest'] = [bool]$Latest.ToString().ToLower() + $body['prerelease'] = $false + $body['draft'] = $false + } + if ($Draft -or $Prerelease) { + $body['make_latest'] = $false + $body['prerelease'] = [bool]$Prerelease + $body['draft'] = [bool]$Draft + } + + $inputObject = @{ + Method = 'PATCH' + APIEndpoint = "/repos/$Owner/$Repository/releases/$ID" + Body = $body + Context = $Context + } + + if ($PSCmdlet.ShouldProcess("release with ID [$ID] in [$Owner/$Repository]", 'Update')) { + $resultLatest = $PSBoundParameters.ContainsKey('Latest') ? $Latest : $release.IsLatest + Invoke-GitHubAPI @inputObject | ForEach-Object { + [GitHubRelease]::new($_.Response , $Owner, $Repository, $resultLatest) + } + } + } + + end { + Write-Debug "[$stackPath] - End" + } +} diff --git a/src/types/GitHubRelease.Types.ps1xml b/src/types/GitHubRelease.Types.ps1xml new file mode 100644 index 000000000..1c1acc885 --- /dev/null +++ b/src/types/GitHubRelease.Types.ps1xml @@ -0,0 +1,12 @@ + + + + GitHubRelease + + + Release + $this.ID + + + + diff --git a/tests/Environments.Tests.ps1 b/tests/Environments.Tests.ps1 index 024be230b..67756b571 100644 --- a/tests/Environments.Tests.ps1 +++ b/tests/Environments.Tests.ps1 @@ -40,8 +40,8 @@ Describe 'Environments' { Write-Host ($context | Format-List | Out-String) } } - $repoPrefix = "$testName-$os-$TokenType-$guid" - $repoName = $repoPrefix + $repoPrefix = "$testName-$os-$TokenType" + $repoName = "$repoPrefix-$guid" $environmentName = "$testName-$os-$TokenType-$guid" switch ($OwnerType) { diff --git a/tests/Releases.Tests.ps1 b/tests/Releases.Tests.ps1 new file mode 100644 index 000000000..210173643 --- /dev/null +++ b/tests/Releases.Tests.ps1 @@ -0,0 +1,647 @@ +#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() + +BeforeAll { + $testName = 'ReleasesTests' + $os = $env:RUNNER_OS + $guid = [guid]::NewGuid().ToString() +} + +Describe 'Releases' { + $authCases = . "$PSScriptRoot/Data/AuthCases.ps1" + + Context 'As using on ' -ForEach $authCases { + BeforeAll { + $context = Connect-GitHubAccount @connectParams -PassThru -Silent + LogGroup 'Context' { + Write-Host ($context | Format-Table | Out-String) + } + if ($AuthType -eq 'APP') { + LogGroup 'Context - Installation' { + $context = Connect-GitHubApp @connectAppParams -PassThru -Default -Silent + Write-Host ($context | Format-Table | Out-String) + } + } + $repoPrefix = "$testName-$os-$TokenType" + $repoName = "$repoPrefix-$guid" + + $params = @{ + Name = $repoName + Context = $context + AllowSquashMerge = $true + AddReadme = $true + License = 'mit' + Gitignore = 'VisualStudio' + } + switch ($OwnerType) { + 'user' { + $repo = New-GitHubRepository @params + } + 'organization' { + $repo = New-GitHubRepository @params -Organization $owner + } + } + LogGroup "Repository - [$repoName]" { + Write-Host ($repo | Format-Table | Out-String) + } + } + + AfterAll { + switch ($OwnerType) { + 'user' { + Get-GitHubRepository | Where-Object { $_.Name -like "$repoPrefix*" } | Remove-GitHubRepository -Confirm:$false + } + 'organization' { + Get-GitHubRepository -Organization $Owner | Where-Object { $_.Name -like "$repoPrefix*" } | Remove-GitHubRepository -Confirm:$false + } + } + Get-GitHubContext -ListAvailable | Disconnect-GitHubAccount -Silent + } + + Context 'Releases' -Skip:($OwnerType -eq 'repository') { + It 'New-GitHubRelease - Creates a new release' { + $release = New-GitHubRelease -Owner $Owner -Repository $repo -Tag 'v1.0' -Latest + LogGroup 'Release' { + Write-Host ($release | Format-List -Property * | Out-String) + } + $release | Should -Not -BeNullOrEmpty + $release | Should -BeOfType 'GitHubRelease' + } + + It 'New-GitHubRelease - Throws when tag already exists' { + { New-GitHubRelease -Owner $Owner -Repository $repo -Tag 'v1.0' -Latest } | Should -Throw + } + + It 'New-GitHubRelease - Creates a new release with a draft' { + $release = New-GitHubRelease -Owner $Owner -Repository $repo -Tag 'v1.2' -Draft + LogGroup 'Release' { + Write-Host ($release | Format-List -Property * | Out-String) + } + $release | Should -Not -BeNullOrEmpty + $release | Should -BeOfType 'GitHubRelease' + $release.IsDraft | Should -BeTrue + $release.IsLatest | Should -BeFalse + $release.IsPrerelease | Should -BeFalse + } + + It 'New-GitHubRelease - Creates a new release with a pre-release' { + $release = New-GitHubRelease -Owner $Owner -Repository $repo -Tag 'v1.1' -Prerelease + LogGroup 'Release' { + Write-Host ($release | Format-List -Property * | Out-String) + } + $release | Should -Not -BeNullOrEmpty + $release | Should -BeOfType 'GitHubRelease' + $release.Tag | Should -Be 'v1.1' + $release.IsDraft | Should -BeFalse + $release.IsLatest | Should -BeFalse + $release.IsPrerelease | Should -BeTrue + } + + It 'New-GitHubRelease - Creates a new release with a name' { + $release = New-GitHubRelease -Owner $Owner -Repository $repo -Tag 'v1.3' -Name 'Test Release' -GenerateReleaseNotes -Latest + LogGroup 'Release' { + Write-Host ($release | Format-List -Property * | Out-String) + } + $release | Should -Not -BeNullOrEmpty + $release | Should -BeOfType 'GitHubRelease' + } + + It 'Get-GitHubRelease - Gets latest release' { + $release = Get-GitHubRelease -Owner $Owner -Repository $repo + LogGroup 'Latest release' { + Write-Host ($release | Format-List -Property * | Out-String) + } + $release | Should -Not -BeNullOrEmpty + $release.Count | Should -Be 1 + $release | Should -BeOfType 'GitHubRelease' + $release.Tag | Should -Be 'v1.3' + $release.IsLatest | Should -BeTrue + } + + It 'Get-GitHubRelease - Gets all releases' { + $releases = Get-GitHubRelease -Owner $Owner -Repository $repo -All + LogGroup 'All releases' { + Write-Host ($releases | Format-List -Property * | Out-String) + } + $releases | Should -Not -BeNullOrEmpty + $releases.Count | Should -BeGreaterThan 1 + $releases | Should -BeOfType 'GitHubRelease' + } + + It 'Get-GitHubRelease - Gets release by tag' { + $release = Get-GitHubRelease -Owner $Owner -Repository $repo -Tag 'v1.2' + LogGroup 'Release' { + Write-Host ($release | Format-List -Property * | Out-String) + } + $release | Should -Not -BeNullOrEmpty + $release.Count | Should -Be 1 + $release | Should -BeOfType 'GitHubRelease' + $release.Tag | Should -Be 'v1.2' + $release.IsLatest | Should -BeFalse + $release.IsDraft | Should -BeTrue + } + + It 'Get-GitHubRelease - Gets release by ID' { + $specificRelease = Get-GitHubRelease -Owner $Owner -Repository $repo -Tag 'v1.0' + $release = Get-GitHubRelease -Owner $Owner -Repository $repo -ID $specificRelease.ID + LogGroup 'Release' { + Write-Host ($release | Format-List -Property * | Out-String) + } + $release | Should -Not -BeNullOrEmpty + $release.Count | Should -Be 1 + $release | Should -BeOfType 'GitHubRelease' + $release.Tag | Should -Be 'v1.0' + $release.IsLatest | Should -BeFalse + $release.IsDraft | Should -BeFalse + $release.IsPrerelease | Should -BeFalse + } + + It 'Get-GitHubRelease - Gets release by ID using Pipeline' { + $specificRelease = Get-GitHubRelease -Owner $Owner -Repository $repo -Tag 'v1.2' + $release = Get-GitHubRelease -Owner $Owner -Repository $repo -ID $specificRelease.ID + LogGroup 'Release' { + Write-Host ($release | Format-List -Property * | Out-String) + } + $release | Should -Not -BeNullOrEmpty + $release.Count | Should -Be 1 + $release | Should -BeOfType 'GitHubRelease' + $release.Tag | Should -Be 'v1.2' + } + + It 'Update-GitHubRelease - Update release v1.0' { + $release = Update-GitHubRelease -Owner $Owner -Repository $repo -Tag 'v1.0' -Name 'Updated Release' -Notes 'Updated release notes' + LogGroup 'Updated release' { + Write-Host ($release | Format-List -Property * | Out-String) + } + $release | Should -Not -BeNullOrEmpty + $release | Should -BeOfType 'GitHubRelease' + $release.Name | Should -Be 'Updated Release' + $release.Notes | Should -Be 'Updated release notes' + $release.Tag | Should -Be 'v1.0' + $release.IsLatest | Should -BeFalse + $release.IsDraft | Should -BeFalse + $release.IsPrerelease | Should -BeFalse + } + + It 'Update-GitHubRelease - Update release v1.1' { + $release = Update-GitHubRelease -Owner $Owner -Repository $repo -Tag 'v1.1' -Name 'Updated Release' -Notes 'Updated release notes' + LogGroup 'Updated release' { + Write-Host ($release | Format-List -Property * | Out-String) + } + $release | Should -Not -BeNullOrEmpty + $release | Should -BeOfType 'GitHubRelease' + $release.Name | Should -Be 'Updated Release' + $release.Notes | Should -Be 'Updated release notes' + $release.Tag | Should -Be 'v1.1' + $release.IsLatest | Should -BeFalse + $release.IsDraft | Should -BeFalse + $release.IsPrerelease | Should -BeTrue + } + + It 'Update-GitHubRelease - Update release v1.2' { + $release = Update-GitHubRelease -Owner $Owner -Repository $repo -Tag 'v1.2' -Name 'Updated Release' -Notes 'Updated release notes' + LogGroup 'Updated release' { + Write-Host ($release | Format-List -Property * | Out-String) + } + $release | Should -Not -BeNullOrEmpty + $release | Should -BeOfType 'GitHubRelease' + $release.Name | Should -Be 'Updated Release' + $release.Notes | Should -Be 'Updated release notes' + $release.IsLatest | Should -BeFalse + $release.IsDraft | Should -BeTrue + $release.IsPrerelease | Should -BeFalse + } + + It 'Update-GitHubRelease - Update release v1.3' { + $release = Update-GitHubRelease -Owner $Owner -Repository $repo -Tag 'v1.3' -Name 'Updated Release' -Notes 'Updated release notes' + LogGroup 'Updated release' { + Write-Host ($release | Format-List -Property * | Out-String) + } + $release | Should -Not -BeNullOrEmpty + $release | Should -BeOfType 'GitHubRelease' + $release.Name | Should -Be 'Updated Release' + $release.Notes | Should -Be 'Updated release notes' + $release.Tag | Should -Be 'v1.3' + $release.IsLatest | Should -BeTrue + $release.IsDraft | Should -BeFalse + $release.IsPrerelease | Should -BeFalse + } + + It 'Set-GitHubRelease - Sets release v1.0 as latest' { + $release = Set-GitHubRelease -Owner $Owner -Repository $repo -Tag 'v1.0' -Latest -Name 'Updated Release again' -Notes 'Updated release notes to something else' + LogGroup 'Set release' { + Write-Host ($release | Format-List -Property * | Out-String) + } + $release | Should -Not -BeNullOrEmpty + $release | Should -BeOfType 'GitHubRelease' + $release.Tag | Should -Be 'v1.0' + $release.IsLatest | Should -BeTrue + $release.IsDraft | Should -BeFalse + $release.IsPrerelease | Should -BeFalse + $release.Name | Should -Be 'Updated Release again' + $release.Notes | Should -Be 'Updated release notes to something else' + } + + It 'Set-GitHubRelease - Sets a new release as latest - v1.4' { + $release = Set-GitHubRelease -Owner $Owner -Repository $repo -Tag 'v1.4' -Latest -Name 'New Release' -Notes 'New release notes' + LogGroup 'Set release' { + Write-Host ($release | Format-List -Property * | Out-String) + } + $release | Should -Not -BeNullOrEmpty + $release | Should -BeOfType 'GitHubRelease' + $release.Tag | Should -Be 'v1.4' + $release.IsLatest | Should -BeTrue + $release.IsDraft | Should -BeFalse + $release.IsPrerelease | Should -BeFalse + $release.Name | Should -Be 'New Release' + $release.Notes | Should -Be 'New release notes' + } + + It 'Remove-GitHubRelease - Removes release v1.0' { + $release = Get-GitHubRelease -Owner $Owner -Repository $repo -Tag 'v1.0' + $release | Should -Not -BeNullOrEmpty + $release.Count | Should -Be 1 + $release | Should -BeOfType 'GitHubRelease' + $release.Tag | Should -Be 'v1.0' + $release.IsLatest | Should -BeFalse + $release.IsDraft | Should -BeFalse + $release.IsPrerelease | Should -BeFalse + + Remove-GitHubRelease -Owner $Owner -Repository $repo -ID $release.ID -Confirm:$false + + $release = Get-GitHubRelease -Owner $Owner -Repository $repo -Tag 'v1.0' + $release | Should -BeNullOrEmpty + } + + It 'New-GitHubReleaseNote - Generates release notes' { + $notes = New-GitHubReleaseNote -Owner $Owner -Repository $repo -Tag 'v1.4' + LogGroup 'Generated release notes' { + Write-Host ($notes | Format-List -Property * | Out-String) + } + $notes | Should -Not -BeNullOrEmpty + $notes.Name | Should -Not -BeNullOrEmpty + $notes.Notes | Should -Not -BeNullOrEmpty + } + + It 'New-GitHubReleaseNote - Generates release notes with custom parameters' { + $releaseTag = 'v1.4' + $previousTag = 'v1.3' + $notes = New-GitHubReleaseNote -Owner $Owner -Repository $repo -Tag $releaseTag -PreviousTag $previousTag -Target 'main' + LogGroup 'Generated release notes with parameters' { + Write-Host ($notes | Format-List -Property * | Out-String) + } + $notes | Should -Not -BeNullOrEmpty + $notes.Name | Should -Not -BeNullOrEmpty + $notes.Notes | Should -Not -BeNullOrEmpty + $notes.Notes | Should -Match $releaseTag + } + } + Context 'Release Assets' -Skip:($OwnerType -eq 'repository') { + BeforeAll { + $testFolderGuid = [Guid]::NewGuid().ToString().Substring(0, 8) + $testFolderName = "GHAssetTest-$testFolderGuid" + $testFolderPath = Join-Path -Path $PSScriptRoot -ChildPath $testFolderName + New-Item -Path $testFolderPath -ItemType Directory -Force + $testFiles = @{ + TextFile = @{ + Name = 'TextFile.txt' + Path = Join-Path -Path $testFolderPath -ChildPath 'TextFile.txt' + Content = @' +This is a simple text file for testing GitHub release assets +'@ + ContentType = 'text/plain' + } + MarkdownFile = @{ + Name = 'Documentation.md' + Path = Join-Path -Path $testFolderPath -ChildPath 'Documentation.md' + Content = @' +# Test Documentation +## Introduction +This is a markdown file used for testing GitHub release assets +'@ + ContentType = 'text/markdown' + } + JsonFile = @{ + Name = 'Config.json' + Path = Join-Path -Path $testFolderPath -ChildPath 'Config.json' + Content = @' +{ + "name": "GitHub Release Asset Test", + "version": "1.0.0", + "description": "Test file for GitHub release assets" +} +'@ + ContentType = 'application/json' + } + XmlFile = @{ + Name = 'Data.xml' + Path = Join-Path -Path $testFolderPath -ChildPath 'Data.xml' + Content = @' + + + Test Item + 100 + + +'@ + ContentType = 'application/xml' + } + CsvFile = @{ + Name = 'Records.csv' + Path = Join-Path -Path $testFolderPath -ChildPath 'Records.csv' + Content = @' +ID,Name,Value +1,Item1,100 +2,Item2,200 +3,Item3,300 +'@ + ContentType = 'text/csv' + } + } + foreach ($file in $testFiles.Values) { + Set-Content -Path $file.Path -Value $file.Content + } + $zipFileName = "$testFolderName.zip" + $zipFilePath = Join-Path -Path $PSScriptRoot -ChildPath $zipFileName + Compress-Archive -Path "$testFolderPath\*" -DestinationPath $zipFilePath -Force + $testFiles['ZipFile'] = @{ + Name = $zipFileName + Path = $zipFilePath + ContentType = 'application/zip' + } + $release = Get-GitHubRelease -Owner $Owner -Repository $repo + } + + It 'Add-GitHubReleaseAsset - Creates a new release asset' { + $asset = $release | Add-GitHubReleaseAsset -Path $testFiles.TextFile.Path + LogGroup 'Added asset' { + Write-Host ($asset | Format-List -Property * | Out-String) + } + $asset | Should -Not -BeNullOrEmpty + $asset | Should -BeOfType 'GitHubReleaseAsset' + $asset.Name | Should -Be $testFiles.TextFile.Name + $asset.Label | Should -Be $testFiles.TextFile.Name + $asset.Size | Should -BeGreaterThan 0 + $asset.ContentType | Should -Be $testFiles.TextFile.ContentType + + $downloadPath = Join-Path -Path $PSScriptRoot -ChildPath "Downloaded-$($testFiles.TextFile.Name)" + Invoke-WebRequest -Uri $asset.Url -OutFile $downloadPath -RetryIntervalSec 5 -MaximumRetryCount 5 + Get-Content -Path $downloadPath | Should -Be $testFiles.TextFile.Content + } + + It 'Add-GitHubReleaseAsset - Creates a release asset with custom parameters' { + $customName = "Custom-$($testFiles.MarkdownFile.Name)" + $label = 'Test Markdown Documentation' + $asset = $release | Add-GitHubReleaseAsset -Path $testFiles.MarkdownFile.Path -Name $customName -Label $label + LogGroup 'Added markdown asset' { + Write-Host ($asset | Format-List -Property * | Out-String) + } + $asset | Should -Not -BeNullOrEmpty + $asset | Should -BeOfType 'GitHubReleaseAsset' + $asset.Name | Should -Be $customName + $asset.ContentType | Should -Be $testFiles.MarkdownFile.ContentType + $asset.Label | Should -Be $label + $asset.Size | Should -BeGreaterThan 0 + + $downloadPath = Join-Path -Path $PSScriptRoot -ChildPath "Downloaded-$customName" + Invoke-WebRequest -Uri $asset.Url -OutFile $downloadPath -RetryIntervalSec 5 -MaximumRetryCount 5 + (Get-Content -Path $downloadPath -Raw) | Should -Match '# Test Documentation' + } + + It 'Add-GitHubReleaseAsset - Adds a folder as a zipped asset to a release' { + $label = 'Test Files Collection' + $asset = $release | Add-GitHubReleaseAsset -Label $label -Path $testFiles.ZipFile.Path + LogGroup 'Added zip asset' { + Write-Host ($asset | Format-List -Property * | Out-String) + } + $asset | Should -Not -BeNullOrEmpty + $asset | Should -BeOfType 'GitHubReleaseAsset' + $asset.Name | Should -Be $testFiles.ZipFile.Name + $asset.Label | Should -Be $label + $asset.ContentType | Should -Be $testFiles.ZipFile.ContentType + $asset.Size | Should -BeGreaterThan 0 + + $downloadPath = Join-Path -Path $PSScriptRoot -ChildPath "Downloaded-$($testFiles.ZipFile.Name)" + Invoke-WebRequest -Uri $asset.Url -OutFile $downloadPath -RetryIntervalSec 5 -MaximumRetryCount 5 + Test-Path -Path $downloadPath | Should -BeTrue + } + + It 'Add-GitHubReleaseAsset - Adds multiple files from a folder to a release' { + $folderName = "FolderAssetTest-$testFolderGuid" + $folderPath = Join-Path -Path $PSScriptRoot -ChildPath $folderName + New-Item -Path $folderPath -ItemType Directory -Force + + $fileContents = @{ + 'config.json' = '{"name": "Test Config", "version": "1.0.0"}' + 'readme.md' = '# Test Folder\nThis is a test folder for uploading to a GitHub release.' + 'data.txt' = 'This is some test data' + } + + foreach ($file in $fileContents.GetEnumerator()) { + $filePath = Join-Path -Path $folderPath -ChildPath $file.Key + Set-Content -Path $filePath -Value $file.Value + } + + $asset = $release | Add-GitHubReleaseAsset -Path $folderPath -Label 'Folder Asset Test' + + LogGroup 'Added folder asset' { + Write-Host ($asset | Format-List -Property * | Out-String) + } + + $asset | Should -Not -BeNullOrEmpty + $asset | Should -BeOfType 'GitHubReleaseAsset' + $asset.Name | Should -Be $folderName + $asset.Label | Should -Be 'Folder Asset Test' + $asset.ContentType | Should -Be 'application/zip' + $asset.Size | Should -BeGreaterThan 0 + + $downloadPath = Join-Path -Path $PSScriptRoot -ChildPath "Downloaded-$folderName.zip" + Invoke-WebRequest -Uri $asset.Url -OutFile $downloadPath -RetryIntervalSec 5 -MaximumRetryCount 5 + + $extractPath = Join-Path -Path $PSScriptRoot -ChildPath "Extract-$folderName" + New-Item -Path $extractPath -ItemType Directory -Force + Expand-Archive -Path $downloadPath -DestinationPath $extractPath -Force + + foreach ($file in $fileContents.GetEnumerator()) { + $extractedFilePath = Join-Path -Path $extractPath -ChildPath $file.Key + Test-Path -Path $extractedFilePath | Should -BeTrue + Get-Content -Path $extractedFilePath | Should -Be $file.Value + } + } + + It 'Get-GitHubReleaseAsset - Gets all assets from a release ID' { + $release = Get-GitHubRelease -Owner $Owner -Repository $repo + $assets = Get-GitHubReleaseAsset -Owner $Owner -Repository $repo -ReleaseID $release.ID + LogGroup 'Release assets by release ID' { + Write-Host ($assets | Format-List -Property * | Out-String) + } + $assets | Should -Not -BeNullOrEmpty + $assets.Count | Should -Be 4 + $assets | Should -BeOfType 'GitHubReleaseAsset' + } + + It 'Get-GitHubReleaseAsset - Gets all assets from the latest release' { + $assets = Get-GitHubReleaseAsset -Owner $Owner -Repository $repo + LogGroup 'Release assets from latest release' { + Write-Host ($assets | Format-List -Property * | Out-String) + } + $assets | Should -Not -BeNullOrEmpty + $assets | Should -BeOfType 'GitHubReleaseAsset' + } + + It 'Get-GitHubReleaseAsset - Gets a specific asset by ID' { + $release = Get-GitHubRelease -Owner $Owner -Repository $repo + $assets = Get-GitHubReleaseAsset -Owner $Owner -Repository $repo -ReleaseID $release.ID + $asset = Get-GitHubReleaseAsset -Owner $Owner -Repository $repo -ID $assets[0].ID + LogGroup 'Release asset by asset ID' { + Write-Host ($asset | Format-List -Property * | Out-String) + } + $asset | Should -Not -BeNullOrEmpty + $asset | Should -BeOfType 'GitHubReleaseAsset' + $asset.ID | Should -Be $assets[0].ID + } + + It 'Get-GitHubReleaseAsset - Gets a specific asset by name from a release ID' { + $release = Get-GitHubRelease -Owner $Owner -Repository $repo + $assets = Get-GitHubReleaseAsset -Owner $Owner -Repository $repo -ReleaseID $release.ID + $assetName = $assets[0].Name + $asset = Get-GitHubReleaseAsset -Owner $Owner -Repository $repo -ReleaseID $release.ID -Name $assetName + LogGroup 'Release asset by name from release ID' { + Write-Host ($asset | Format-List -Property * | Out-String) + } + $asset | Should -Not -BeNullOrEmpty + $asset | Should -BeOfType 'GitHubReleaseAsset' + $asset.Name | Should -Be $assetName + } + + It 'Get-GitHubReleaseAsset - Gets a specific asset by name from a tag' { + $release = Get-GitHubRelease -Owner $Owner -Repository $repo + $assets = Get-GitHubReleaseAsset -Owner $Owner -Repository $repo -ReleaseID $release.ID + $assetName = $assets[0].Name + $asset = Get-GitHubReleaseAsset -Owner $Owner -Repository $repo -Tag $release.Tag -Name $assetName + LogGroup 'Release asset by name from tag' { + Write-Host ($asset | Format-List -Property * | Out-String) + } + $asset | Should -Not -BeNullOrEmpty + $asset | Should -BeOfType 'GitHubReleaseAsset' + $asset.Name | Should -Be $assetName + } + + It 'Update-GitHubReleaseAsset - Updates a release asset' { + $release = Get-GitHubRelease -Owner $Owner -Repository $repo + $assets = Get-GitHubReleaseAsset -Owner $Owner -Repository $repo -ReleaseID $release.ID + $newLabel = 'Updated test asset' + $asset = Update-GitHubReleaseAsset -Owner $Owner -Repository $repo -ID $assets[0].ID -Label $newLabel + LogGroup 'Updated asset' { + Write-Host ($asset | Format-List -Property * | Out-String) + } + $asset | Should -Not -BeNullOrEmpty + $asset | Should -BeOfType 'GitHubReleaseAsset' + $asset.Label | Should -Be $newLabel + } + + It 'Remove-GitHubReleaseAsset - Removes a release asset' { + $release = Get-GitHubRelease -Owner $Owner -Repository $repo + $assets = Get-GitHubReleaseAsset -Owner $Owner -Repository $repo -ReleaseID $release.ID + $assetID = $assets[0].ID + Remove-GitHubReleaseAsset -Owner $Owner -Repository $repo -ID $assetID -Confirm:$false + $updatedAssets = Get-GitHubReleaseAsset -Owner $Owner -Repository $repo -ReleaseID $release.ID + $remainingAsset = $updatedAssets | Where-Object { $_.ID -eq $assetID } + $remainingAsset | Should -BeNullOrEmpty + } + + It 'Save-GitHubReleaseAsset - Downloads a release asset by ID' { + $release = Get-GitHubRelease -Owner $Owner -Repository $repo + $assets = Get-GitHubReleaseAsset -Owner $Owner -Repository $repo -ReleaseID $release.ID + $downloadPath = Join-Path -Path $PSScriptRoot -ChildPath "DownloadTest-$testFolderGuid" + New-Item -Path $downloadPath -ItemType Directory -Force | Out-Null + + $downloadedFile = Save-GitHubReleaseAsset -Owner $Owner -Repository $repo -ID $assets[0].ID -Path $downloadPath -PassThru + LogGroup 'Downloaded Asset' { + Write-Host ($downloadedFile | Format-List | Out-String) + } + + $downloadedFile | Should -Not -BeNullOrEmpty + Test-Path -Path $downloadedFile.FullName | Should -BeTrue + $downloadedFile.Name | Should -Be $assets[0].Name + } + + It 'Save-GitHubReleaseAsset - Downloads a release asset by name from a tag' { + $release = Get-GitHubRelease -Owner $Owner -Repository $repo + $assets = Get-GitHubReleaseAsset -Owner $Owner -Repository $repo -ReleaseID $release.ID + $assetName = $assets[1].Name + $downloadPath = Join-Path -Path $PSScriptRoot -ChildPath "DownloadByName-$testFolderGuid" + New-Item -Path $downloadPath -ItemType Directory -Force | Out-Null + + $downloadedFile = Save-GitHubReleaseAsset -Owner $Owner -Repository $repo -Tag $release.Tag -Name $assetName -Path $downloadPath -PassThru + LogGroup 'Downloaded Asset by Name' { + Write-Host ($downloadedFile | Format-List | Out-String) + } + + $downloadedFile | Should -Not -BeNullOrEmpty + Test-Path -Path $downloadedFile.FullName | Should -BeTrue + $downloadedFile.Name | Should -Be $assetName + } + + It 'Save-GitHubReleaseAsset - Downloads and extracts a ZIP release asset' { + $release = Get-GitHubRelease -Owner $Owner -Repository $repo + $zipAsset = Get-GitHubReleaseAsset -Owner $Owner -Repository $repo -ReleaseID $release.ID | + Where-Object { $_.Name -like '*.zip' } | + Select-Object -First 1 + + if ($zipAsset) { + $extractPath = Join-Path -Path $PSScriptRoot -ChildPath "ExtractTest-$testFolderGuid" + New-Item -Path $extractPath -ItemType Directory -Force | Out-Null + + $extractedItems = Save-GitHubReleaseAsset -Owner $Owner -Repository $repo -ID $zipAsset.ID -Path $extractPath -Expand -PassThru + LogGroup 'Extracted ZIP Asset' { + Write-Host ($extractedItems | Format-Table | Out-String) + } + + $extractedItems | Should -Not -BeNullOrEmpty + Test-Path -Path $extractPath | Should -BeTrue + Test-Path -Path (Join-Path -Path $extractPath -ChildPath $zipAsset.Name) | Should -BeFalse + (Get-ChildItem -Path $extractPath -Recurse).Count | Should -BeGreaterThan 0 + } else { + Set-ItResult -Inconclusive -Because 'No ZIP asset found for testing extraction' + } + } + + It 'Save-GitHubReleaseAsset - Uses pipeline input from Get-GitHubReleaseAsset' { + $release = Get-GitHubRelease -Owner $Owner -Repository $repo + $asset = Get-GitHubReleaseAsset -Owner $Owner -Repository $repo -ReleaseID $release.ID | + Select-Object -First 1 + + $pipelinePath = Join-Path -Path $PSScriptRoot -ChildPath "PipelineTest-$testFolderGuid" + New-Item -Path $pipelinePath -ItemType Directory -Force | Out-Null + + $downloadedFile = $asset | Save-GitHubReleaseAsset -Path $pipelinePath -PassThru + LogGroup 'Downloaded Asset via Pipeline' { + Write-Host ($downloadedFile | Format-List | Out-String) + } + + $downloadedFile | Should -Not -BeNullOrEmpty + Test-Path -Path $downloadedFile.FullName | Should -BeTrue + $downloadedFile.Name | Should -Be $asset.Name + } + } + } +} diff --git a/tests/Secrets.Tests.ps1 b/tests/Secrets.Tests.ps1 index 6d909b181..1735e8942 100644 --- a/tests/Secrets.Tests.ps1 +++ b/tests/Secrets.Tests.ps1 @@ -36,21 +36,22 @@ Describe 'Secrets' { Write-Host ($context | Format-List | Out-String) } } - $repoPrefix = "$testName-$os-$TokenType-$guid" + $repoPrefix = "$testName-$os-$TokenType" + $repoName = "$repoPrefix-$guid" $secretPrefix = ("$testName`_$os`_$TokenType`_$guid" -replace '-', '_').ToUpper() $orgSecretName = "$secretPrefix`ORG" $environmentName = "$testName-$os-$TokenType-$guid" switch ($OwnerType) { 'user' { - $repo = New-GitHubRepository -Name "$repoPrefix-1" -AllowSquashMerge - $repo2 = New-GitHubRepository -Name "$repoPrefix-2" -AllowSquashMerge - $repo3 = New-GitHubRepository -Name "$repoPrefix-3" -AllowSquashMerge + $repo = New-GitHubRepository -Name "$repoName-1" -AllowSquashMerge + $repo2 = New-GitHubRepository -Name "$repoName-2" -AllowSquashMerge + $repo3 = New-GitHubRepository -Name "$repoName-3" -AllowSquashMerge } 'organization' { - $repo = New-GitHubRepository -Organization $owner -Name "$repoPrefix-1" -AllowSquashMerge - $repo2 = New-GitHubRepository -Organization $owner -Name "$repoPrefix-2" -AllowSquashMerge - $repo3 = New-GitHubRepository -Organization $owner -Name "$repoPrefix-3" -AllowSquashMerge + $repo = New-GitHubRepository -Organization $owner -Name "$repoName-1" -AllowSquashMerge + $repo2 = New-GitHubRepository -Organization $owner -Name "$repoName-2" -AllowSquashMerge + $repo3 = New-GitHubRepository -Organization $owner -Name "$repoName-3" -AllowSquashMerge LogGroup "Org secret - [$secretPrefix]" { $params = @{ Owner = $owner @@ -65,7 +66,7 @@ Describe 'Secrets' { } } } - LogGroup "Repository - [$repoPrefix]" { + LogGroup "Repository - [$repoName]" { Write-Host ($repo | Format-List | Out-String) Write-Host ($repo2 | Format-List | Out-String) Write-Host ($repo3 | Format-List | Out-String) diff --git a/tests/Variables.Tests.ps1 b/tests/Variables.Tests.ps1 index edaef1add..2d3112860 100644 --- a/tests/Variables.Tests.ps1 +++ b/tests/Variables.Tests.ps1 @@ -36,21 +36,22 @@ Describe 'Variables' { Write-Host ($context | Format-List | Out-String) } } - $repoPrefix = "$testName-$os-$TokenType-$guid" + $repoPrefix = "$testName-$os-$TokenType" + $repoName = "$repoPrefix-$guid" $variablePrefix = ("$testName`_$os`_$TokenType`_$guid" -replace '-', '_').ToUpper() $orgVariableName = "$variablePrefix`ORG" $environmentName = "$testName-$os-$TokenType-$guid" switch ($OwnerType) { 'user' { - $repo = New-GitHubRepository -Name "$repoPrefix-1" -AllowSquashMerge - $repo2 = New-GitHubRepository -Name "$repoPrefix-2" -AllowSquashMerge - $repo3 = New-GitHubRepository -Name "$repoPrefix-3" -AllowSquashMerge + $repo = New-GitHubRepository -Name "$repoName-1" -AllowSquashMerge + $repo2 = New-GitHubRepository -Name "$repoName-2" -AllowSquashMerge + $repo3 = New-GitHubRepository -Name "$repoName-3" -AllowSquashMerge } 'organization' { - $repo = New-GitHubRepository -Organization $owner -Name "$repoPrefix-1" -AllowSquashMerge - $repo2 = New-GitHubRepository -Organization $owner -Name "$repoPrefix-2" -AllowSquashMerge - $repo3 = New-GitHubRepository -Organization $owner -Name "$repoPrefix-3" -AllowSquashMerge + $repo = New-GitHubRepository -Organization $owner -Name "$repoName-1" -AllowSquashMerge + $repo2 = New-GitHubRepository -Organization $owner -Name "$repoName-2" -AllowSquashMerge + $repo3 = New-GitHubRepository -Organization $owner -Name "$repoName-3" -AllowSquashMerge LogGroup "Org variable - [$variablePrefix]" { $params = @{ Owner = $owner @@ -64,7 +65,7 @@ Describe 'Variables' { } } } - LogGroup "Repository - [$repoPrefix]" { + LogGroup "Repository - [$repoName]" { Write-Host ($repo | Format-Table | Out-String) Write-Host ($repo2 | Format-Table | Out-String) Write-Host ($repo3 | Format-Table | Out-String)