windows-terminal/tools/ReleaseEngineering/Draft-TerminalReleases.ps1

309 lines
9.3 KiB
PowerShell

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
#################################
# Draft-TerminalReleases takes a directory full of Terminal build outputs:
# - zip files
# - preinstallation kits
# - MSIX bundles
# and produces tagged releases with hashes and assets on GitHub.
# It automatically cracks files to get their version numbers, arranges them into branding categories,
# and publishes drafts based on their contents.
#Requires -Version 7.0
#Requires -Modules PSGitHub
[CmdletBinding(SupportsShouldProcess)]
Param(
[string[]]$Directory,
[string]$Owner = "microsoft",
[string]$RepositoryName = "terminal",
[switch]$DumpOutput = $false
)
$ErrorActionPreference = 'Stop'
$PSDefaultParameterValues['*GitHub*:Owner'] = $Owner
$PSDefaultParameterValues['*GitHub*:RepositoryName'] = $RepositoryName
Enum AssetType {
Unknown
ApplicationBundle
PreinstallKit
Zip
}
Enum Branding {
Unknown
Release
Preview
Canary
Dev
}
$script:tar = "tar"
Class Asset {
[string]$Name
[Version]$Version
[string]$ExpandedVersion
[AssetType]$Type
[Branding]$Branding
[string]$Architecture
[string]$Path
[string]$VersionMoniker
Asset([string]$Path) {
$this.Path = $Path;
}
[void]Load() {
$local:bundlePath = $this.Path
$local:sentinelFile = New-TemporaryFile -Confirm:$false -WhatIf:$false
$local:directory = New-Item -Type Directory "$($local:sentinelFile.FullName)_Package" -Confirm:$false -WhatIf:$false
Remove-Item $local:sentinelFile -Force -EA:Ignore -Confirm:$false -WhatIf:$false
$local:ext = [IO.Path]::GetExtension($this.Path)
$local:filename = [IO.Path]::GetFileName($this.Path)
If (".zip" -eq $local:ext -and $local:filename -like '*Preinstall*') {
Write-Verbose "Cracking Preinstall Kit $($this.Path)"
$local:licenseFile = & $script:tar -t -f $this.Path | Select-String "_License1.xml"
If (-Not $local:licenseFile) {
Throw ("$($this.Path) does not appear to be a preinstall kit")
}
$local:bundleName = ($local:licenseFile -Split "_")[0] + ".msixbundle"
Write-Verbose "Found inner bundle $($local:bundleName)"
& $script:tar -x -f $this.Path -C $local:directory $local:bundleName
$local:bundlePath = Join-Path $local:directory $local:bundleName
$this.Type = [AssetType]::PreinstallKit
$this.Architecture = "all"
} ElseIf (".zip" -eq $local:ext) {
$this.Type = [AssetType]::Zip
} ElseIf (".msixbundle" -eq $local:ext) {
$this.Type = [AssetType]::ApplicationBundle
$this.Architecture = "all"
}
If ($this.Type -Ne [AssetType]::Zip) {
Write-Verbose "Cracking bundle $($local:bundlePath)"
$local:firstMsixName = & $script:tar -t -f $local:bundlePath |
Select-String 'Cascadia.*\.msix' |
Select-Object -First 1 -Expand Line
& $script:tar -x -f $local:bundlePath -C $local:directory $local:firstMsixName
Write-Verbose "Found inner msix $($local:firstMsixName)"
$local:msixPath = Join-Path $local:directory $local:firstMsixName
& $script:tar -x -v -f $local:msixPath -C $local:directory AppxManifest.xml
Write-Verbose "Parsing AppxManifest.xml"
$local:Manifest = [xml](Get-Content (Join-Path $local:directory AppxManifest.xml))
$this.ParseManifest($local:Manifest)
} Else {
& $script:tar -x -f $this.Path -C $local:directory --strip-components=1 '*/wt.exe'
$this.ExpandedVersion = (Get-Item (Join-Path $local:directory wt.exe)).VersionInfo.ProductVersion
# Zip files just encode everything in their filename. Not great, but workable.
$this.ParseFilename($local:filename)
}
$v = [version]$this.Version
$this.VersionMoniker = "{0}.{1}" -f ($v.Major, $v.Minor)
$this.Branding = Switch($this.Name) {
"Microsoft.WindowsTerminal" { [Branding]::Release }
"Microsoft.WindowsTerminalPreview" { [Branding]::Preview }
"Microsoft.WindowsTerminalCanary" { [Branding]::Canary }
"WindowsTerminalDev" { [Branding]::Dev }
Default { [Branding]::Unknown }
}
}
[void]ParseManifest([xml]$Manifest) {
$this.Name = $Manifest.Package.Identity.Name
$this.Version = $Manifest.Package.Identity.Version
}
[void]ParseFilename([string]$filename) {
$parts = [IO.Path]::GetFileNameWithoutExtension($filename).Split("_")
$this.Name = $parts[0]
$this.Version = $parts[1]
$this.Architecture = $parts[2]
}
[string]IdealFilename() {
$local:bundleName = "{0}_{1}_8wekyb3d8bbwe.msixbundle" -f ($this.Name, $this.Version)
$local:filename = Switch($this.Type) {
PreinstallKit {
"{0}_Windows10_PreinstallKit.zip" -f $local:bundleName
}
ApplicationBundle {
$local:bundleName
}
Zip {
"{0}_{1}_{2}.zip" -f ($this.Name, $this.Version, $this.Architecture)
}
Default {
Throw "Unknown type $($_.Type)"
}
}
return $local:filename
}
static [Asset] CreateFromFile([string]$Path) {
$a = [Asset]::new($Path)
$a.Load()
return $a
}
}
class Release {
[string]$Name
[Branding]$Branding
[Version]$TagVersion
[Version]$DisplayVersion
[string]$Branch
[Asset[]]$Assets
Release([Asset[]]$a) {
$this.Assets = $a
$this.Branding = $a[0].Branding
$this.Name = Switch($this.Branding) {
Release { "Windows Terminal" }
Preview { "Windows Terminal Preview" }
Default { throw "Unknown Branding for release publication $_" }
}
$local:minVer = $a.Version | Measure -Minimum | Select-Object -Expand Minimum
$this.TagVersion = $local:minVer
$this.DisplayVersion = $local:minVer
$this.Branch = "release-{0}.{1}" -f ($local:minVer.Major, $local:minVer.Minor)
}
[string]TagName() {
return "v{0}" -f $this.TagVersion
}
}
enum TaggingType {
TagAlreadyExists
TagBranchLatest
TagCommitSha1
}
class ReleaseConfig {
[Release]$Release
[TaggingType]$TaggingType
[string]$TagTarget # may be null
}
Function Read-ReleaseConfigFromHost([Release]$Release) {
$choices = @(
[System.Management.Automation.Host.ChoiceDescription]::new("No New &Tag", "Tag already exists, do not create a new tag"),
[System.Management.Automation.Host.ChoiceDescription]::new("Tag &Branch"),
[System.Management.Automation.Host.ChoiceDescription]::new("Tag &Commit")
)
$existingTagSha1 = & git rev-parse --verify $Release.TagName() 2>$null
If($existingTagSha1) {
### If there is already a tag, trust the release engineer.
Write-Verbose "Found existing tag $($Release.TagName())"
return [ReleaseConfig]@{
Release = $Release;
TaggingType = [TaggingType]::TagAlreadyExists;
TagTarget = $null;
}
}
$choice = $Host.UI.PromptForChoice("How should I tag $("{0} v{1}" -f ($Release.Name,$Release.DisplayVersion))?", $null, $choices, 0)
$target = $null
Switch($choice) {
0 { }
1 { $target = $Release.Branch }
2 { $target = Read-Host "What commit corresponds to $($Release.DisplayVersion)?" }
}
Return [ReleaseConfig]@{
Release = $Release;
TaggingType = [TaggingType]$choice;
TagTarget = $target;
}
}
Function New-ReleaseBody([Release]$Release) {
$zipAssetVersion = $Release.Assets.ExpandedVersion | ? { $_.Length -Gt 0 } | Select -First 1
$body = "---`n`n"
If (-Not [String]::IsNullOrEmpty($zipAssetVersion)) {
$body += "_Binary files inside the unpackaged distribution archive bear the version number ``$zipAssetVersion``._`n`n"
}
$body += "### Asset Hashes`n`n";
ForEach($a in $Release.Assets) {
$body += "- {0}`n - SHA256 ``{1}```n" -f ($a.IdealFilename(), (Get-FileHash $a.Path -Algorithm SHA256 | Select-Object -Expand Hash))
}
Return $body
}
# Collect Assets from $Directory, figure out what those assets are
$Assets = Get-ChildItem $Directory -Recurse -Include *.msixbundle, *.zip | ForEach-Object {
[Asset]::CreateFromFile($_.FullName)
}
$Releases = $Assets | Group VersionMoniker | ForEach-Object {
[Release]::new($_.Group)
}
$ReleaseConfigs = $Releases | % { Read-ReleaseConfigFromHost $_ }
If($DumpOutput) {
$ReleaseConfigs
Return
}
$sentinelFile = New-TemporaryFile -Confirm:$false -WhatIf:$false
$directory = New-Item -Type Directory "$($sentinelFile.FullName)_PackageUploads" -Confirm:$false -WhatIf:$false
Remove-Item $sentinelFile -Force -EA:Ignore -Confirm:$false -WhatIf:$false
ForEach($c in $ReleaseConfigs) {
$releaseName = "{0} v{1}" -f ($c.Release.Name, $c.Release.DisplayVersion)
if($PSCmdlet.ShouldProcess("Release $releaseName", "Publish")) {
Write-Verbose "Preparing release $releaseName"
$releaseParams = @{
TagName = $c.Release.TagName();
Name = $releaseName;
}
If($c.TagTarget) {
Switch($c.TaggingType) {
[TagCommitSha1] { $releaseParams.CommitSHA = $c.TagTarget }
[TagBranchLatest] { $releaseParams.Branch = $c.Release.Branch }
}
Write-Verbose " - Will create tag $($c.Release.TagName()) to point to $($c.TagTarget)"
} Else {
Write-Verbose " - Tag already exists"
}
$releaseParams.PreRelease = Switch($c.Release.Branding) {
Release { $false }
Preview { $true }
}
$releaseParams.Body = New-ReleaseBody $c.Release
$GitHubRelease = New-GitHubRelease @releaseParams -Draft:$true
ForEach($a in $c.Release.Assets) {
$idealPath = (Join-Path $directory $a.IdealFilename())
Copy-Item $a.Path $idealPath -Confirm:$false
Write-Verbose "Uploading $idealPath to Release $($GitHubRelease.id)"
New-GitHubReleaseAsset -ReleaseId $GitHubRelease.id -Path $idealPath
}
}
}