diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 55f6da6..941f91a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -140,11 +140,6 @@ jobs: $key = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($env:APPCAST_SIGNATURE_KEY_BASE64)) Set-Content -Path $keyPath -Value $key - # Download the old appcast from GCS. - $oldAppCastPath = Join-Path $env:TEMP "appcast.old.xml" - & gsutil cp $env:APPCAST_GCS_URI $oldAppCastPath - if ($LASTEXITCODE -ne 0) { throw "Failed to download appcast" } - # Generate the new appcast and signature. $newAppCastPath = Join-Path $env:TEMP "appcast.new.xml" $newAppCastSignaturePath = $newAppCastPath + ".signature" @@ -154,7 +149,6 @@ jobs: -x64Path "${{ steps.release.outputs.X64_OUTPUT_PATH }}" ` -arm64Path "${{ steps.release.outputs.ARM64_OUTPUT_PATH }}" ` -keyPath $keyPath ` - -inputAppCastPath $oldAppCastPath ` -outputAppCastPath $newAppCastPath ` -outputAppCastSignaturePath $newAppCastSignaturePath if ($LASTEXITCODE -ne 0) { throw "Failed to generate new appcast" } diff --git a/scripts/Update-AppCast.ps1 b/scripts/Update-AppCast.ps1 index 904dcdf..9327351 100644 --- a/scripts/Update-AppCast.ps1 +++ b/scripts/Update-AppCast.ps1 @@ -1,4 +1,4 @@ -# Updates appcast.xml and appcast.xml.signature for a given release. +# Generates appcast.xml and appcast.xml.signature for a given release. # # Requires openssl.exe. You can install it via winget: # winget install ShiningLight.OpenSSL.Light @@ -9,7 +9,6 @@ # -x64Path # -arm64Path # -keyPath -# -inputAppCastPath # -outputAppCastPath # -outputAppCastSignaturePath param ( @@ -37,10 +36,6 @@ param ( [ValidateScript({ Test-Path $_ })] [string] $keyPath, - [Parameter(Mandatory = $false)] - [ValidateScript({ Test-Path $_ })] - [string] $inputAppCastPath = "appcast.xml", - [Parameter(Mandatory = $false)] [string] $outputAppCastPath = "appcast.xml", @@ -104,86 +99,73 @@ Get-Content $releaseNotesHtmlPath Write-Output "---- End of Release Notes HTML ----" Write-Output "" -[xml] $appCast = Get-Content $inputAppCastPath - -# Set up namespace manager for sparkle: prefix -$nsManager = New-Object System.Xml.XmlNamespaceManager($appCast.NameTable) -$nsManager.AddNamespace("sparkle", "http://www.andymatuschak.org/xml-namespaces/sparkle") +$sparkleNs = "http://www.andymatuschak.org/xml-namespaces/sparkle" +$releaseNotesHtml = [System.IO.File]::ReadAllText($releaseNotesHtmlPath) -# Find the matching channel item -$channelItem = $appCast.SelectSingleNode("//item[sparkle:channel='$channel']", $nsManager) -if ($null -eq $channelItem) { - throw "Could not find channel item for channel: $channel" -} - -# Update the item properties -$channelItem.title = $tag -$channelItem.pubDate = $pubDate -$channelItem.SelectSingleNode("sparkle:version", $nsManager).InnerText = $version -$channelItem.SelectSingleNode("sparkle:shortVersionString", $nsManager).InnerText = $version -$channelItem.SelectSingleNode("sparkle:fullReleaseNotesLink", $nsManager).InnerText = "https://github.com/$repo/releases" - -# Set description with proper line breaks -$descriptionNode = $channelItem.SelectSingleNode("description") -$descriptionNode.InnerXml = "" # Clear existing content -$cdata = $appCast.CreateCDataSection([System.IO.File]::ReadAllText($releaseNotesHtmlPath)) -$descriptionNode.AppendChild($cdata) | Out-Null - -# Remove existing enclosures -$existingEnclosures = $channelItem.SelectNodes("enclosure") -foreach ($enclosure in $existingEnclosures) { - $channelItem.RemoveChild($enclosure) | Out-Null -} - -# Add new enclosures -$enclosures = @( - @{ - path = $x64Path - os = "win-x64" - }, - @{ - path = $arm64Path - os = "win-arm64" - } +# NetSparkle only reads the first enclosure of each item, so every +# architecture gets its own item with a single enclosure. +$installers = @( + @{ path = $x64Path; os = "win-x64" }, + @{ path = $arm64Path; os = "win-arm64" } ) -foreach ($enclosure in $enclosures) { - $fileName = Split-Path $enclosure.path -Leaf - $url = "https://github.com/$repo/releases/download/$tag/$fileName" - $fileSize = (Get-Item $enclosure.path).Length - $signature = Get-Ed25519Signature $enclosure.path - - $newEnclosure = $appCast.CreateElement("enclosure") - $newEnclosure.SetAttribute("url", $url) - $newEnclosure.SetAttribute("type", "application/x-msdos-program") - $newEnclosure.SetAttribute("length", $fileSize) - - # Set namespaced attributes - $sparkleNs = $nsManager.LookupNamespace("sparkle") - $attrs = @{ - "os" = $enclosure.os - "version" = $version - "shortVersionString" = $version - "criticalUpdate" = "false" - "edSignature" = $signature # NetSparkle prefers edSignature over signature - } - foreach ($key in $attrs.Keys) { - $attr = $appCast.CreateAttribute("sparkle", $key, $sparkleNs) - $attr.Value = $attrs[$key] - $newEnclosure.Attributes.Append($attr) | Out-Null + +# The appcast only lists the latest release, so it is generated from scratch. +# NetSparkle fails to parse CRLF line endings, so use LF. +$settings = New-Object System.Xml.XmlWriterSettings +$settings.Indent = $true +$settings.IndentChars = " " +$settings.NewLineChars = "`n" +$settings.Encoding = New-Object System.Text.UTF8Encoding($false) +$writer = [System.Xml.XmlWriter]::Create($outputAppCastPath, $settings) +try { + $writer.WriteStartDocument($true) + $writer.WriteStartElement("rss") + $writer.WriteAttributeString("xmlns", "sparkle", $null, $sparkleNs) + $writer.WriteAttributeString("version", "2.0") + $writer.WriteStartElement("channel") + $writer.WriteElementString("title", "Coder Desktop") + $writer.WriteElementString("language", "en") + + foreach ($installer in $installers) { + $fileName = Split-Path $installer.path -Leaf + + $writer.WriteStartElement("item") + $writer.WriteElementString("title", $tag) + $writer.WriteStartElement("description") + $writer.WriteCData($releaseNotesHtml) + $writer.WriteEndElement() + $writer.WriteElementString("pubDate", $pubDate) + $writer.WriteElementString("channel", $sparkleNs, $channel) + $writer.WriteElementString("version", $sparkleNs, $version) + $writer.WriteElementString("shortVersionString", $sparkleNs, $version) + $writer.WriteElementString("fullReleaseNotesLink", $sparkleNs, "https://github.com/$repo/releases") + + $writer.WriteStartElement("enclosure") + $writer.WriteAttributeString("url", "https://github.com/$repo/releases/download/$tag/$fileName") + $writer.WriteAttributeString("type", "application/x-msdos-program") + $writer.WriteAttributeString("length", (Get-Item $installer.path).Length) + $writer.WriteAttributeString("os", $sparkleNs, $installer.os) + $writer.WriteAttributeString("version", $sparkleNs, $version) + $writer.WriteAttributeString("shortVersionString", $sparkleNs, $version) + $writer.WriteAttributeString("criticalUpdate", $sparkleNs, "false") + # NetSparkle prefers edSignature over signature + $writer.WriteAttributeString("edSignature", $sparkleNs, (Get-Ed25519Signature $installer.path)) + $writer.WriteEndElement() + + $writer.WriteEndElement() } - $channelItem.AppendChild($newEnclosure) | Out-Null + $writer.WriteEndElement() + $writer.WriteEndElement() + $writer.WriteEndDocument() +} +finally { + $writer.Close() } -# Save the updated XML. Convert CRLF to LF since CRLF seems to break NetSparkle -$appCast.Save($outputAppCastPath) -$content = [System.IO.File]::ReadAllText($outputAppCastPath) -$content = $content -replace "`r`n", "`n" -[System.IO.File]::WriteAllText($outputAppCastPath, $content) - -Write-Output "---- Updated appcast -----" +Write-Output "---- Generated appcast -----" Get-Content $outputAppCastPath -Write-Output "---- End of updated appcast ----" +Write-Output "---- End of generated appcast ----" Write-Output "" # Generate the signature for the appcast itself