diff --git a/tools/ChangelogTools.psm1 b/tools/ChangelogTools.psm1 new file mode 100644 index 0000000000..d6bcadab4f --- /dev/null +++ b/tools/ChangelogTools.psm1 @@ -0,0 +1,401 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +#requires -Version 6.0 + +using module .\GitHubTools.psm1 + +class IgnoreConfiguration +{ + [string[]]$User + [string[]]$IssueLabel + [string[]]$PRLabel + [string[]]$CommitLabel +} + +class ChangeInfo +{ + [GitHubCommitInfo]$Commit + [GitHubPR]$PR + [GitHubIssue[]]$ClosedIssues + [int]$IssueNumber = -1 + [int]$PRNumber = -1 + [string]$ContributingUser + [string]$BodyText + [string]$Subject +} + +class ChangelogEntry +{ + [uri]$IssueLink + [uri]$PRLink + [string]$Category + [string[]]$Tags + [string]$BodyText + [string]$Subject + [string]$Thanks + [string]$RepositoryName + [ChangeInfo]$Change +} + +class ChangeLog +{ + ChangeLog() + { + $this.Sections = [System.Collections.Generic.Dictionary[string, ChangelogEntry]]::new() + } + + [string]$ReleaseName + [datetime]$Date + [string]$Preamble + [System.Collections.Generic.Dictionary[string, ChangelogEntry]]$Sections +} + +function NormalizeSubject +{ + [OutputType([string])] + param( + [Parameter(Mandatory)] + [string] + $Subject + ) + + $Subject = $Subject.Trim() + if ([char]::IsLower($Subject[0])) { $Subject = [char]::ToUpper($Subject[0]) + $Subject.Substring(1) } + if ($Subject[$Subject.Length] -ne '.') { $Subject += '.' } + + return $Subject +} + +filter Get-ChangeInfoFromCommit +{ + [OutputType([ChangeInfo])] + param( + [Parameter(Mandatory, ValueFromPipeline, Position=0)] + [GitHubCommitInfo[]] + $Commit, + + [Parameter(Mandatory)] + [string] + $GitHubToken + ) + + foreach ($singleCommit in $Commit) + { + Write-Verbose "Getting change information for commit $($Commit.Hash)" + + $changelogItem = [ChangeInfo]@{ + Commit = $singleCommit + BodyText = $singleCommit.Body + Subject = $singleCommit.Subject + ContributingUser = $singleCommit.GitHubCommitData.author.login + } + + if ($Commit.PRNumber -ge 0) + { + $getPrParams = @{ + Organization = $singleCommit.Organization + Repository = $singleCommit.Repository + PullNumber = $singleCommit.PRNumber + GitHubToken = $GitHubToken + } + $pr = Get-GitHubPR @getPrParams + + $changelogItem.PR = $pr + $changelogItem.PRNumber = $pr.Number + + $closedIssueInfos = $pr.GetClosedIssueInfos() + if ($closedIssueInfos) + { + $changelogItem.ClosedIssues = $closedIssueInfos | Get-GitHubIssue + $changelogItem.IssueNumber = $closedIssueInfos[0].Number + } + } + + $changelogItem + } +} + +filter New-ChangelogEntry +{ + [OutputType([ChangelogEntry])] + param( + [Parameter(Mandatory, ValueFromPipeline)] + [ChangeInfo] + $Change, + + [Parameter(Mandatory)] + [System.Collections.Specialized.OrderedDictionary] + $EntryCategories, + + [Parameter(Mandatory)] + [string] + $DefaultCategory, + + [Parameter(Mandatory)] + [hashtable] + $TagLabels, + + [Parameter()] + [string[]] + $NoThanks = @() + ) + + [string[]]$tags = @() + :labelLoop foreach ($issueLabel in $Change.ClosedIssues.Labels) + { + if (-not $entryCategory) + { + foreach ($category in $EntryCategories.GetEnumerator()) + { + if ($issueLabel -in $category.Value.Issue) + { + $entryCategory = $category.Key + continue :labelLoop + } + } + } + + $tag = $TagLabels[$issueLabel] + if ($tag) + { + $tags += $tag + } + } + + if (-not $entryCategory) + { + $entryCategory = $DefaultCategory + } + + $organization = $Change.Commit.Organization + $repository = $Change.Commit.Repository + + $issueLink = if ($Change.IssueNumber -ge 0) { $Change.ClosedIssues[0].GetHtmlUri() } else { $null } + $prLink = if ($Change.PRNumber -ge 0) { "https://github.com/$organization/$repository/pull/$($Change.PRNumber)" } else { $null } + $thanks = if ($Change.ContributingUser -notin $NoThanks) { $Change.ContributingUser } else { $null } + + $subject = $Change.Subject + if ($subject -match '(.*)\(#\d+\)$') + { + $subject = $Matches[1] + } + + Write-Verbose "Assembled changelog entry for commit $($Change.Commit.Hash)" + + return [ChangelogEntry]@{ + IssueLink = $issueLink + PRLink = $prLink + Thanks = $thanks + Category = $entryCategory + Tags = $tags + Change = $Change + RepositoryName = "$organization/$repository" + BodyText = $Change.BodyText + Subject = $subject + } +} + +function New-ChangeLogSection +{ + [OutputType([string])] + param( + [Parameter(Mandatory, ValueFromPipeline)] + [ChangelogEntry[]] + $ChangelogEntry, + + [Parameter(Mandatory)] + [string] + $ReleaseName, + + [Parameter(Mandatory)] + [string[]] + $Categories, + + [Parameter(Mandatory)] + [string] + $DefaultCategory, + + [Parameter()] + [string] + $Preamble, + + [Parameter()] + [string] + $Postamble, + + [Parameter()] + [datetime] + $Date = [datetime]::Now, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] + $DateFormat = 'dddd, dd MM yyyy', + + [Parameter()] + [string] + $Indent = ' ' + ) + + begin + { + $entries = [ordered]@{} + + foreach ($category in $Categories) + { + $entries[$category] = [System.Collections.Generic.List[ChangelogEntry]]::new() + } + } + + process + { + foreach ($entry in $ChangelogEntry) + { + $entries[$entry.Category].Add($entry) + } + } + + end + { + $dateStr = $Date.ToString($DateFormat) + $sb = [System.Text.StringBuilder]::new().AppendLine("## $ReleaseName").AppendLine("### $dateStr") + + if ($Preamble) + { + [void]$sb.AppendLine($Preamble) + } + + [void]$sb.AppendLine() + + foreach ($category in $entries.GetEnumerator()) + { + if (-not $category.Value) + { + continue + } + + if ($category.Key -ne $DefaultCategory) + { + [void]$sb.AppendLine("$($category.Key):") + } + + foreach ($item in $category.Value) + { + # Set up the pieces needed for a changelog entry + $link = if ($item.PRLink) { $item.PRLink } else { $org = $item.Change.Commit.Organization; "https://github.com/$org/$project" } + $thanks = $item.Thanks + + if ($item.Change.IssueNumber -ge 0) + { + $project = $item.Change.ClosedIssues[0].Repository + $issueNumber = $item.Change.IssueNumber + } + elseif ($item.Change.PRNumber -ge 0) + { + $project = $item.Change.PR.Repository + $issueNumber = $item.Change.PRNumber + } + + # Add the list bullet + [void]$sb.Append('- ') + + # Start with the tags + if ($item.Tags) + { + [void]$sb.Append(($item.Tags -join ' ')).Append(' ') + } + + # Create a header for the change if there is an issue number + if ($issueNumber) + { + [void]$sb.AppendLine("[$project #$issueNumber]($link) -").Append($Indent) + } + + [void]$sb.Append((NormalizeSubject -Subject $item.Subject)) + if ($thanks) + { + [void]$sb.Append(" (Thanks @$thanks!)") + } + [void]$sb.AppendLine() + } + } + + if ($Postamble) + { + [void]$sb.AppendLine().AppendLine($Postamble) + } + + [void]$sb.AppendLine() + + return $sb.ToString() + } +} + +filter Skip-IgnoredChange +{ + param( + [Parameter(Mandatory, ValueFromPipeline)] + [ChangeInfo[]] + $Change, + + [Parameter()] + [string] + $User, + + [Parameter()] + [string] + $CommitLabel, + + [Parameter()] + [string[]] + $IssueLabel, + + [Parameter()] + [string[]] + $PRLabel + ) + + :outer foreach ($chg in $Change) + { + $msg = $chg.Subject + if ($chg.ContributingUser -in $User) + { + $u = $chg.ContributingUser + Write-Verbose "Skipping change from user '$u': '$msg'" + continue + } + + foreach ($chgCommitLabel in $chg.Commit.CommitLabels) + { + if ($chgCommitLabel -in $CommitLabel) + { + Write-Verbose "Skipping change with commit label '$chgCommitLabel': '$msg'" + continue outer + } + } + + foreach ($chgIssueLabel in $chg.ClosedIssues.Labels) + { + if ($chgIssueLabel -in $IssueLabel) + { + Write-Verbose "Skipping change with issue label '$chgIssueLabel': '$msg'" + continue outer + } + } + + foreach ($chgPRLabel in $chg.PR.Labels) + { + if ($chgPRLabel -in $PRLabel) + { + Write-Verbose "Skipping change with PR label '$chgPRLabel': '$msg'" + continue outer + } + } + + # Yield the change + $chg + } +} + +Export-ModuleMember -Function Get-ChangeInfoFromCommit,New-ChangelogEntry,New-ChangelogSection,Skip-IgnoredChange diff --git a/tools/GitHubTools.psm1 b/tools/GitHubTools.psm1 index 7d348ea8bf..75db3e5110 100644 --- a/tools/GitHubTools.psm1 +++ b/tools/GitHubTools.psm1 @@ -1,23 +1,216 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -function GetHumanishRepositoryName +#requires -Version 6.0 + +class GitCommitInfo +{ + [string]$Hash + [string[]]$ParentHashes + [string]$Subject + [string]$Body + [string]$ContributorName + [string]$ContributorEmail + [string[]]$CommitLabels + [int]$PRNumber = -1 +} + +class GitHubCommitInfo : GitCommitInfo +{ + [string]$Organization + [string]$Repository + [pscustomobject]$GitHubCommitData +} + +class GitHubIssueInfo +{ + [int]$Number + [string]$Organization + [string]$Repository + + [uri]GetHtmlUri() + { + return [uri]"https://github.com/$($this.Organization)/$($this.Repository)/issues/$($this.Number)" + } + + [uri]GetApiUri() + { + return [uri]"https://api.github.com/repos/$($this.Organization)/$($this.Repository)/issues/$($this.Number)" + } +} + +class GitHubIssue : GitHubIssueInfo +{ + [pscustomobject]$RawResponse + [string]$Body + [string[]]$Labels +} + +class GitHubPR : GitHubIssue +{ + hidden [GitHubIssueInfo[]]$ClosedIssues = $null + + [GitHubIssueInfo[]]GetClosedIssueInfos() + { + if ($null -eq $this.ClosedIssues) + { + $this.ClosedIssues = $this.Body | + GetClosedIssueUrisInBodyText | + GetGitHubIssueFromUri + } + + return $this.ClosedIssues + } + + [uri]GetHtmlUri() + { + return [uri]"https://github.com/$($this.Organization)/$($this.Repository)/pull/$($this.Number)" + } + + [uri]GetApiUri() + { + return [uri]"https://api.github.com/repos/$($this.Organization)/$($this.Repository)/pulls/$($this.Number)" + } +} + +function GetGitHubHeaders +{ + param( + [Parameter()] + [string] + $GitHubToken, + + [Parameter()] + [string] + $Accept + ) + + $headers = @{} + + if ($GitHubToken) + { + $headers.Authorization = "token $GitHubToken" + } + + if ($Accept) + { + $headers.Accept = $Accept + } + + return $headers +} + +$script:CloseKeywords = @( + 'close' + 'closes' + 'closed' + 'fix' + 'fixes' + 'fixed' + 'resolve' + 'resolves' + 'resolved' +) +$script:EndNonCharRegex = [regex]::new('[^0-9]*$', 'compiled') +filter GetClosedIssueUrisInBodyText +{ + param( + [Parameter(ValueFromPipeline)] + [string] + $Text + ) + + $words = $Text.Split() + + $expectIssue = $false + for ($i = 0; $i -lt $words.Length; $i++) + { + $currWord = $words[$i] + + if ($script:CloseKeywords -contains $currWord) + { + $expectIssue = $true + continue + } + + if (-not $expectIssue) + { + continue + } + + $expectIssue = $false + + $trimmedWord = $script:EndNonCharRegex.Replace($currWord, '') + + if ([uri]::IsWellFormedUriString($trimmedWord, 'Absolute')) + { + # Yield + [uri]$trimmedWord + } + } +} + +filter GetGitHubIssueFromUri +{ + param( + [Parameter(ValueFromPipeline)] + [uri] + $IssueUri + ) + + if ($IssueUri.Authority -ne 'github.com') + { + return + } + + if ($IssueUri.Segments.Length -ne 5) + { + return + } + + if ($IssueUri.Segments[3] -ne 'issues/') + { + return + } + + $issueNum = -1 + if (-not [int]::TryParse($IssueUri.Segments[4], [ref]$issueNum)) + { + return + } + + return [GitHubIssueInfo]@{ + Organization = $IssueUri.Segments[1].TrimEnd('/') + Repository = $IssueUri.Segments[2].TrimEnd('/') + Number = $issueNum + } +} + +filter GetHumanishRepositoryDetails { param( [string] - $Repository + $RemoteUrl ) - if ($Repository.EndsWith('.git')) + if ($RemoteUrl.EndsWith('.git')) { - $Repository = $Repository.Substring(0, $Repository.Length - 4) + $RemoteUrl = $RemoteUrl.Substring(0, $RemoteUrl.Length - 4) } else { - $Repository = $Repository.Trim('/') + $RemoteUrl = $RemoteUrl.Trim('/') } - return $Repository.Substring($Repository.LastIndexOf('/') + 1) + $lastSlashIdx = $RemoteUrl.LastIndexOf('/') + $repository = $RemoteUrl.Substring($lastSlashIdx + 1) + $secondLastSlashIdx = $RemoteUrl.LastIndexOfAny(('/', ':'), $lastSlashIdx - 1) + $organization = $RemoteUrl.Substring($secondLastSlashIdx + 1, $lastSlashIdx - $secondLastSlashIdx - 1) + + return @{ + Organization = $organization + Repository = $repository + } } function Exec @@ -53,7 +246,7 @@ function Copy-GitRepository [Parameter()] [ValidateNotNullOrEmpty()] [string] - $Destination = (GetHumanishRepositoryName $OriginRemote), + $Destination = ((GetHumanishRepositoryDetails $OriginRemote).Repository), [Parameter()] [string] @@ -95,25 +288,35 @@ function Copy-GitRepository New-Item -Path $containingDir -ItemType Directory -ErrorAction Stop } - Exec { git clone --single-branch --branch $CloneBranch $OriginRemote $Destination } + Write-Verbose "Cloning git repository '$OriginRemote' to path '$Destination'" + + Exec { git clone $OriginRemote --branch $CloneBranch --single-branch $Destination } + + if ($CloneBranch) + { + Write-Verbose "Cloned branch: $CloneBranch" + } Push-Location $Destination try { Exec { git config core.autocrlf true } - foreach ($remote in $Remotes.get_Keys()) - { - Exec { git remote add $remote $Remotes[$remote] } - } - - if ($PullUpstream -and $remote['upstream']) + if ($Remotes) { - Exec { git pull upstream $CloneBranch } + foreach ($remote in $Remotes.get_Keys()) + { + Exec { git remote add $remote $Remotes[$remote] } + } - if ($UpdateOrigin) + if ($PullUpstream -and $Remotes['upstream']) { - Exec { git push origin "+$CloneBranch"} + Exec { git pull upstream $CloneBranch } + + if ($UpdateOrigin) + { + Exec { git push origin "+$CloneBranch"} + } } } @@ -139,9 +342,9 @@ function Submit-GitChanges [string] $Branch, - [Parameter(Mandatory)] + [Parameter()] [string] - $RepositoryLocation, + $RepositoryLocation = (Get-Location), [Parameter()] [string[]] @@ -173,6 +376,9 @@ function Submit-GitChanges { Exec { git add -A } } + + Write-Verbose "Commiting and pushing changes in '$RepositoryLocation' to '$Remote/$Branch'" + Exec { git commit -m $Message } Exec { git push $Remote $Branch } } @@ -182,6 +388,120 @@ function Submit-GitChanges } } +function Get-GitCommit +{ + [OutputType([GitHubCommitInfo])] + [CmdletBinding(DefaultParameterSetName='SinceRef')] + param( + [Parameter(Mandatory, ParameterSetName='SinceRef')] + [Alias('SinceBranch', 'SinceTag')] + [string] + $SinceRef, + + [Parameter(ParameterSetName='SinceRef')] + [Alias('UntilBranch', 'UntilTag')] + [string] + $UntilRef = 'HEAD', + + [Parameter()] + [string] + $Remote, + + [Parameter()] + [string] + $GitHubToken, + + [Parameter()] + [string] + $RepositoryPath + ) + + if ($RepositoryPath) + { + Push-Location $RepositoryPath + } + try + { + if (-not $Remote) + { + $Remote = 'upstream' + try + { + $null = Exec { git remote get-url $Remote } + } + catch + { + $Remote = 'origin' + } + } + + $originDetails = GetHumanishRepositoryDetails -RemoteUrl (Exec { git remote get-url $Remote }) + $organization = $originDetails.Organization + $repository = $originDetails.Repository + + Write-Verbose "Getting local git commit data" + + $null = Exec { git fetch --all } + + $lastCommonCommit = Exec { git merge-base $SinceRef $UntilRef } + + $format = '%H||%P||%aN||%aE||%s' + $commits = Exec { git --no-pager log "$lastCommonCommit..$UntilRef" --format=$format } + + $irmParams = if ($GitHubToken) + { + @{ Headers = GetGitHubHeaders -GitHubToken $GitHubToken -Accept 'application/vnd.github.v3+json' } + } + else + { + @{ Headers = GetGitHubHeaders -Accept 'application/vnd.github.v3+json' } + } + + return $commits | + ForEach-Object { + $hash,$parents,$name,$email,$subject = $_.Split('||') + $body = (Exec { git --no-pager show $hash -s --format=%b }) -join "`n" + $commitVal = [GitHubCommitInfo]@{ + Hash = $hash + ParentHashes = $parents + ContributorName = $name + ContributorEmail = $email + Subject = $subject + Body = $body + Organization = $organization + Repository = $repository + } + + # Query the GitHub API for more commit information + Write-Verbose "Querying GitHub api for data on commit $hash" + $commitVal.GitHubCommitData = Invoke-RestMethod -Method Get -Uri "https://api.github.com/repos/$organization/$repository/commits/$hash" @irmParams + + # Look for something like 'This is a commit message (#1224)' + $pr = [regex]::Match($subject, '\(#(\d+)\)$').Groups[1].Value + if ($pr) + { + $commitVal.PRNumber = $pr + } + + # Look for something like '[Ignore] [giraffe] Fix tests' + $commitLabels = [regex]::Matches($subject, '^(\[(.*?)\]\s*)*') + if ($commitLabels.Groups.Length -ge 3 -and $commitLabels.Groups[2].Captures.Value) + { + $commitVal.CommitLabels = $commitLabels.Groups[2].Captures.Value + } + + $commitVal + } + } + finally + { + if ($RepositoryPath) + { + Pop-Location + } + } +} + function New-GitHubPR { param( @@ -232,14 +552,121 @@ function New-GitHubPR maintainer_can_modify = $true } | ConvertTo-Json - $headers = @{ - Accept = 'application/vnd.github.v3+json' - Authorization = "token $GitHubToken" - } + $headers = GetGitHubHeaders -GitHubToken $GitHubToken -Accept 'application/vnd.github.v3+json' + Write-Verbose "Opening new GitHub pull request on '$Organization/$Repository' with title '$Title'" Invoke-RestMethod -Method Post -Uri $uri -Body $body -Headers $headers } +function Get-GitHubPR +{ + param( + [Parameter(Mandatory)] + [string] + $Organization, + + [Parameter(Mandatory)] + [string] + $Repository, + + [Parameter(Mandatory)] + [int[]] + $PullNumber, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] + $GitHubToken + ) + + return $PullNumber | + ForEach-Object { + $params = @{ + Method = 'Get' + Uri = "https://api.github.com/repos/$Organization/$Repository/pulls/$_" + } + + if ($GitHubToken) + { + $params.Headers = GetGitHubHeaders -GitHubToken $GitHubToken + } + + Write-Verbose "Retrieving GitHub pull request #$_" + + $prResponse = Invoke-RestMethod @params + + [GitHubPR]@{ + RawResponse = $prResponse + Number = $prResponse.Number + Organization = $Organization + Repository = $Repository + Body = $prResponse.body + Labels = $prResponse.labels.name + } + } +} + +filter Get-GitHubIssue +{ + [CmdletBinding(DefaultParameterSetName='IssueInfo')] + param( + [Parameter(Mandatory, ValueFromPipeline, Position=0, ParameterSetName='IssueInfo')] + [GitHubIssueInfo[]] + $IssueInfo, + + [Parameter(Mandatory, ParameterSetName='Params')] + [string] + $Organization, + + [Parameter(Mandatory, ParameterSetName='Params')] + [string] + $Repository, + + [Parameter(Mandatory, ParameterSetName='Params')] + [int] + $Number, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] + $GitHubToken + ) + + foreach ($issue in $IssueInfo) + { + if (-not $issue) + { + $issue = [GitHubIssueInfo]@{ + Organization = $Organization + Repository = $Repository + Number = $Number + } + } + + $irmParams = @{ + Method = 'Get' + Uri = $IssueInfo.GetApiUri() + } + + if ($GitHubToken) + { + $irmParams.Headers = GetGitHubHeaders -GitHubToken $GitHubToken + } + + Write-Verbose "Retrieving GitHub issue #$($issue.Number)" + $issueResponse = Invoke-RestMethod @irmParams + + return [GitHubIssue]@{ + Organization = $issue.Organization + Repository = $issue.Repository + Number = $issue.Number + RawResponse = $issueResponse + Body = $issueResponse.body + Labels = $issueResponse.labels.name + } + } +} + function Publish-GitHubRelease { param( @@ -298,10 +725,9 @@ function Publish-GitHubRelease $restBody = ConvertTo-Json -InputObject $restParams $uri = "https://api.github.com/repos/$Organization/$Repository/releases" - $headers = @{ - Accept = 'application/vnd.github.v3+json' - Authorization = "token $GitHubToken" - } + $headers = GetGitHubHeaders -GitHubToken $GitHubToken -Accept 'application/vnd.github.v3+json' + + Write-Verbose "Publishing GitHub release '$ReleaseName' to $Organization/$Repository" $response = Invoke-RestMethod -Method Post -Uri $uri -Body $restBody -Headers $headers @@ -328,9 +754,10 @@ function Publish-GitHubRelease } $assetUri = "${assetBaseUri}?name=$fileName" - $headers = @{ - Authorization = "token $GitHubToken" - } + $headers = GetGitHubHeaders -GitHubToken $GitHubToken + + Write-Verbose "Uploading release asset '$fileName' to release '$ReleaseName' in $Organization/$Repository" + # This can be very slow, but it does work $null = Invoke-RestMethod -Method Post -Uri $assetUri -InFile $asset -ContentType $contentType -Headers $headers } @@ -338,4 +765,12 @@ function Publish-GitHubRelease return $response } -Export-ModuleMember -Function Copy-GitRepository,Submit-GitChanges,New-GitHubPR,Publish-GitHubRelease +Export-ModuleMember -Function @( + 'Copy-GitRepository', + 'Submit-GitChanges', + 'Get-GitCommit', + 'New-GitHubPR', + 'Get-GitHubPR', + 'Get-GitHubIssue', + 'Publish-GitHubRelease' +) diff --git a/tools/changelog/updateChangelog.ps1 b/tools/changelog/updateChangelog.ps1 new file mode 100644 index 0000000000..daf0fefd9f --- /dev/null +++ b/tools/changelog/updateChangelog.ps1 @@ -0,0 +1,350 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +#requires -Version 6.0 + +using module ..\GitHubTools.psm1 +using module ..\ChangelogTools.psm1 + +<# +.EXAMPLE +.\updateChangelog.ps1 -GitHubToken $ghTok -PSExtensionSinceRef v2019.5.0 -PsesSinceRef v2.0.0-preview.4 -PSExtensionVersion 2019.9.0 -PsesVersion 2.0.0-preview.5 -PSExtensionUntilRef master -PsesUntilRef master -Verbose +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string] + $GitHubToken, + + [Parameter(Mandatory)] + [string] + $PSExtensionSinceRef, + + [Parameter(Mandatory)] + [string] + $PsesSinceRef, + + [Parameter()] + [version] + $PSExtensionVersion, # Default from package.json + + [Parameter()] + [semver] + $PsesVersion, # Default from PowerShellEditorServices.Common.props + + [Parameter()] + [string] + $PSExtensionReleaseName, # Default from $PSExtensionVersion + + [Parameter()] + [string] + $PsesReleaseName, # Default from $PsesVersion + + [Parameter()] + [string] + $PSExtensionUntilRef = 'HEAD', + + [Parameter()] + [string] + $PsesUntilRef = 'HEAD', + + [Parameter()] + [string] + $PSExtensionBaseBranch, # Default is master if HEAD, otherwise $PSExtensionSinceRef + + [Parameter()] + [string] + $PsesBaseBranch, # Default is master if HEAD, otherwise $PsesSinceRef + + [Parameter()] + [string] + $Organization = 'PowerShell', + + [Parameter()] + [string] + $TargetFork = $Organization, + + [Parameter()] + [string] + $FromFork = 'rjmholt', + + [Parameter()] + [string] + $ChangelogName = 'CHANGELOG.md', + + [Parameter()] + [string] + $PSExtensionRepositoryPath = (Resolve-Path "$PSScriptRoot/../../"), + + [Parameter()] + [string] + $PsesRepositoryPath = (Resolve-Path "$PSExtensionRepositoryPath/../PowerShellEditorServices") +) + +$PSExtensionRepositoryPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($PSExtensionRepositoryPath) +$PsesRepositoryPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($PsesRepositoryPath) + +$packageJson = Get-Content -Raw "$PSExtensionRepositoryPath/package.json" | ConvertFrom-Json +$extensionName = $packageJson.name +if (-not $PSExtensionVersion) +{ + $PSExtensionVersion = $packageJson.version +} + +if (-not $PsesVersion) +{ + $psesProps = [xml](Get-Content -Raw "$PsesRepositoryPath/PowerShellEditorServices.Common.props") + $psesVersionPrefix = $psesProps.Project.PropertyData.VersionPrefix + $psesVersionSuffix = $psesProps.Project.PropertyData.VersionSuffix + + $PsesVersion = [semver]"$psesVersionPrefix-$psesVersionSuffix" +} + +if (-not $PSExtensionReleaseName) +{ + $PSExtensionReleaseName = "v$PSExtensionVersion" +} + +if (-not $PsesReleaseName) +{ + $PsesReleaseName = "v$PsesVersion" +} + +if (-not $PSExtensionBaseBranch) +{ + $PSExtensionBaseBranch = if ($PSExtensionUntilRef -eq 'HEAD') + { + 'master' + } + else + { + $PSExtensionUntilRef + } +} + +if (-not $PsesBaseBranch) +{ + $PsesBaseBranch = if ($PsesUntilRef -eq 'HEAD') + { + 'master' + } + else + { + $PsesUntilRef + } +} + +function UpdateChangelogFile +{ + param( + [Parameter(Mandatory)] + [string] + $NewSection, + + [Parameter(Mandatory)] + [string] + $Path + ) + + Write-Verbose "Writing new changelog section to '$Path'" + + $changelogLines = Get-Content -Path $Path + $newContent = ($changelogLines[0..1] -join "`n`n") + $NewSection + ($changelogLines[2..$changelogLines.Length] -join "`n") + Set-Content -Encoding utf8NoBOM -Value $newContent -Path $Path +} + +#region Configuration + +Write-Verbose "Configuring settings" + +$vscodeRepoName = 'vscode-PowerShell' +$psesRepoName = 'PowerShellEditorServices' + +$dateFormat = 'dddd, MMMM dd, yyyy' + +$ignore = @{ + User = 'dependabot[bot]' + CommitLabel = 'Ignore' +} + +$noThanks = @( + 'rjmholt' + 'TylerLeonhardt' + 'daxian-dbw' + 'SteveL-MSFT' + 'PaulHigin' +) + +$categories = [ordered]@{ + Debugging = @{ + Issue = 'Area-Debugging' + } + CodeLens = @{ + Issue = 'Area-CodeLens' + } + 'Script Analysis' = @{ + Issue = 'Area-Script Analysis' + } + Formatting = @{ + Issue = 'Area-Formatting' + } + 'Integrated Console' = @{ + Issue = 'Area-Integrated Console','Area-PSReadLine' + } + Intellisense = @{ + Issue = 'Area-Intellisense' + } + General = @{ + Issue = 'Area-General' + } +} + +$defaultCategory = 'General' + +$branchName = "changelog-$PSExtensionReleaseName" + +#endregion Configuration + +#region PSES Changelog + +$psesGetCommitParams = @{ + SinceRef = $PsesSinceRef + UntilRef = $PsesUntilRef + GitHubToken = $GitHubToken + RepositoryPath = $PsesRepositoryPath + Verbose = $VerbosePreference +} + +$clEntryParams = @{ + EntryCategories = $categories + DefaultCategory = $defaultCategory + TagLabels = @{ + 'Issue-Enhancement' = '✨' + 'Issue-Bug' = '🐛' + 'Issue-Performance' = '⚡️' + 'Area-Build & Release' = '👷' + 'Area-Code Formatting' = '💎' + 'Area-Configuration' = '🔧' + 'Area-Debugging' = '🔍' + 'Area-Documentation' = '📖' + 'Area-Engine' = '🚂' + 'Area-Folding' = '📚' + 'Area-Integrated Console' = '📟' + 'Area-IntelliSense' = '🧠' + 'Area-Logging' = '💭' + 'Area-Pester' = '🐢' + 'Area-Script Analysis' = '👮‍' + 'Area-Snippets' = '✂️' + 'Area-Startup' = '🛫' + 'Area-Symbols & References' = '🔗' + 'Area-Tasks' = '✅' + 'Area-Test' = '🚨' + 'Area-Threading' = '⏱️' + 'Area-UI' = '📺' + 'Area-Workspaces' = '📁' + } + NoThanks = $noThanks + Verbose = $VerbosePreference +} + +$clSectionParams = @{ + Categories = $categories.Keys + DefaultCategory = $defaultCategory + DateFormat = $dateFormat + Verbose = $VerbosePreference +} + +Write-Verbose "Creating PSES changelog" + +$psesChangelogSection = Get-GitCommit @psesGetCommitParams | + Get-ChangeInfoFromCommit -GitHubToken $GitHubToken -Verbose:$VerbosePreference | + Skip-IgnoredChange @ignore -Verbose:$VerbosePreference | + New-ChangelogEntry @clEntryParams | + New-ChangelogSection @clSectionParams -ReleaseName $PsesReleaseName + +Write-Host "PSES CHANGELOG:`n`n$psesChangelogSection`n`n" + +#endregion PSES Changelog + +#region vscode-PowerShell Changelog +$psesChangelogPostamble = $psesChangelogSection -split "`n" +$psesChangelogPostamble = @("#### [$psesRepoName](https://github.com/$Organization/$psesRepoName)") + $psesChangelogPostamble[2..($psesChangelogPostamble.Length-3)] +$psesChangelogPostamble = $psesChangelogPostamble -join "`n" + +$psExtGetCommitParams = @{ + SinceRef = $PSExtensionSinceRef + UntilRef = $PSExtensionUntilRef + GitHubToken = $GitHubToken + RepositoryPath = $PSExtensionRepositoryPath + Verbose = $VerbosePreference +} +$psextChangelogSection = Get-GitCommit @psExtGetCommitParams | + Get-ChangeInfoFromCommit -GitHubToken $GitHubToken -Verbose:$VerbosePreference | + Skip-IgnoredChange @ignore -Verbose:$VerbosePreference | + New-ChangelogEntry @clEntryParams | + New-ChangelogSection @clSectionParams -Preamble "#### [$vscodeRepoName](https://github.com/$Organization/$vscodeRepoName)" -Postamble $psesChangelogPostamble -ReleaseName $PSExtensionReleaseName + +Write-Host "vscode-PowerShell CHANGELOG:`n`n$psextChangelogSection`n`n" + +#endregion vscode-PowerShell Changelog + +#region PRs + +# PSES PR +$cloneLocation = Join-Path ([System.IO.Path]::GetTempPath()) "${psesRepoName}_changelogupdate" + +$cloneParams = @{ + OriginRemote = "https://github.com/$FromFork/$psesRepoName" + Destination = $cloneLocation + CheckoutBranch = $branchName + CloneBranch = $PsesBaseBranch + Clobber = $true + Remotes = @{ 'upstream' = "https://github.com/$TargetFork/$vscodeRepoName" } +} +Copy-GitRepository @cloneParams -Verbose:$VerbosePreference + +UpdateChangelogFile -NewSection $psesChangelogSection -Path "$cloneLocation/$ChangelogName" + +Submit-GitChanges -RepositoryLocation $cloneLocation -File $GalleryFileName -Branch $branchName -Message "Update CHANGELOG for $PsesReleaseName" -Verbose:$VerbosePreference + +$prParams = @{ + Organization = $TargetFork + Repository = $psesRepoName + Branch = $branchName + Title = "Update CHANGELOG for $PsesReleaseName" + GitHubToken = $GitHubToken + FromOrg = $FromFork + TargetBranch = $PsesBaseBranch +} +New-GitHubPR @prParams -Verbose:$VerbosePreference + +# vscode-PowerShell PR +$cloneLocation = Join-Path ([System.IO.Path]::GetTempPath()) "${vscodeRepoName}_changelogupdate" + +$cloneParams = @{ + OriginRemote = "https://github.com/$FromFork/$vscodeRepoName" + Destination = $cloneLocation + CheckoutBranch = $branchName + CloneBranch = $PSExtensionBaseBranch + Clobber = $true + Remotes = @{ 'upstream' = "https://github.com/$TargetFork/$vscodeRepoName" } + PullUpstream = $true +} +Copy-GitRepository @cloneParams -Verbose:$VerbosePreference + +UpdateChangelogFile -NewSection $psextChangelogSection -Path "$cloneLocation/$ChangelogName" + +Submit-GitChanges -RepositoryLocation $cloneLocation -File $GalleryFileName -Branch $branchName -Message "Update CHANGELOG for $PSExtensionReleaseName" -Verbose:$VerbosePreference + +$prParams = @{ + Organization = $TargetFork + Repository = $vscodeRepoName + Branch = $branchName + Title = "Update $extensionName CHANGELOG for $PSExtensionReleaseName" + GitHubToken = $GitHubToken + FromOrg = $FromFork + TargetBranch = $PSExtensionBaseBranch +} +New-GitHubPR @prParams -Verbose:$VerbosePreference + +#endregion PRs diff --git a/tools/postReleaseScripts/publishGHRelease.ps1 b/tools/postReleaseScripts/publishGHRelease.ps1 index 435c2f715c..db467d3190 100644 --- a/tools/postReleaseScripts/publishGHRelease.ps1 +++ b/tools/postReleaseScripts/publishGHRelease.ps1 @@ -34,14 +34,11 @@ Import-Module "$PSScriptRoot/../GitHubTools.psm1" -Force <# .SYNOPSIS Get the release description from the CHANGELOG - .DESCRIPTION Gets the latest CHANGELOG entry from the CHANGELOG for use as the GitHub release description - .PARAMETER ChangelogPath Path to the changelog file #> - function GetDescriptionFromChangelog { param(