714 lines
22 KiB
PowerShell
714 lines
22 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Builds, signs, verifies, and deploys MoonWell Launcher to production.
|
|
|
|
.DESCRIPTION
|
|
Builds the Windows application and Inno Setup installer, verifies the DSA
|
|
signing key pair, signs the installer, generates appcast.xml, uploads the
|
|
installer to Yandex Object Storage, and publishes AppCast through the MoonWell
|
|
service API. Secrets are read only from environment variables.
|
|
|
|
.PARAMETER ExpectedVersion
|
|
Fails unless the X.Y.Z part of pubspec.yaml matches this value.
|
|
|
|
.PARAMETER ReleaseNotes
|
|
Text written to the AppCast item description.
|
|
|
|
.PARAMETER EnvFile
|
|
Local dotenv file used to fill environment variables that are not already set.
|
|
|
|
.PARAMETER DryRun
|
|
Builds, signs, and verifies local artifacts without changing production.
|
|
|
|
.PARAMETER Force
|
|
Allows redeploying a version that is not newer than the public AppCast.
|
|
|
|
.PARAMETER SkipChecks
|
|
Skips pub get, formatting, static analysis, and tests.
|
|
|
|
.EXAMPLE
|
|
.\tool\deploy_launcher.ps1 -DryRun -ExpectedVersion 1.0.2 `
|
|
-ReleaseNotes "MoonWell Launcher improvements."
|
|
|
|
.EXAMPLE
|
|
.\tool\deploy_launcher.ps1 -ExpectedVersion 1.0.2 `
|
|
-ReleaseNotes "MoonWell Launcher improvements."
|
|
#>
|
|
#requires -Version 5.1
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[string]$ExpectedVersion,
|
|
[string]$ReleaseNotes = 'MoonWell Launcher update.',
|
|
[string]$ApiBaseUrl = 'https://moon-well.online',
|
|
[string]$AppcastUrl = 'https://moon-well.online/appcast.xml',
|
|
[string]$S3Endpoint,
|
|
[string]$S3Bucket,
|
|
[string]$S3Key = 'moonwell_launcher_setup.exe',
|
|
[string]$EnvFile = '.env',
|
|
[string]$PrivateKeyPath = '.moonwell_signing\dsa_priv.pem',
|
|
[string]$PublicKeyPath = 'windows\runner\resources\dsa_pub.pem',
|
|
[string]$FlutterCommand = 'flutter',
|
|
[string]$DartCommand = 'dart',
|
|
[string]$InnoSetupCommand,
|
|
[string]$OpenSslCommand,
|
|
[switch]$SkipChecks,
|
|
[switch]$DryRun,
|
|
[switch]$Force
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$script:SparkleNamespace =
|
|
'http://www.andymatuschak.org/xml-namespaces/sparkle'
|
|
|
|
function Write-Step {
|
|
param([Parameter(Mandatory = $true)][string]$Message)
|
|
|
|
Write-Host "`n==> $Message" -ForegroundColor Cyan
|
|
}
|
|
|
|
function Resolve-RequiredCommand {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$Command,
|
|
[Parameter(Mandatory = $true)][string]$Description
|
|
)
|
|
|
|
if (Test-Path -LiteralPath $Command -PathType Leaf) {
|
|
return (Resolve-Path -LiteralPath $Command).Path
|
|
}
|
|
|
|
$resolved = Get-Command $Command -ErrorAction SilentlyContinue
|
|
if ($null -eq $resolved) {
|
|
throw "$Description was not found: $Command"
|
|
}
|
|
|
|
return $resolved.Source
|
|
}
|
|
|
|
function Invoke-RequiredCommand {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$Command,
|
|
[Parameter(Mandatory = $true)][string[]]$Arguments
|
|
)
|
|
|
|
& $Command @Arguments
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Command failed with exit code ${LASTEXITCODE}: $Command"
|
|
}
|
|
}
|
|
|
|
function Get-FileSha256 {
|
|
param([Parameter(Mandatory = $true)][string]$Path)
|
|
|
|
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
}
|
|
|
|
function Import-DotEnv {
|
|
param([Parameter(Mandatory = $true)][string]$Path)
|
|
|
|
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
|
return
|
|
}
|
|
|
|
$allowedNames = @(
|
|
'AWS_ACCESS_KEY_ID',
|
|
'AWS_SECRET_ACCESS_KEY',
|
|
'AWS_DEFAULT_REGION',
|
|
'AWS_BUCKET',
|
|
'AWS_ENDPOINT',
|
|
'AWS_USE_PATH_STYLE_ENDPOINT',
|
|
'LAUNCHER_AUTH_KEY'
|
|
)
|
|
|
|
foreach ($line in Get-Content -LiteralPath $Path) {
|
|
$trimmed = $line.Trim()
|
|
if (
|
|
[string]::IsNullOrWhiteSpace($trimmed) -or
|
|
$trimmed.StartsWith('#')
|
|
) {
|
|
continue
|
|
}
|
|
|
|
$separator = $trimmed.IndexOf('=')
|
|
if ($separator -le 0) {
|
|
throw "Invalid dotenv line in ${Path}: $line"
|
|
}
|
|
|
|
$name = $trimmed.Substring(0, $separator).Trim()
|
|
if ($allowedNames -notcontains $name) {
|
|
continue
|
|
}
|
|
|
|
$value = $trimmed.Substring($separator + 1).Trim()
|
|
if (
|
|
$value.Length -ge 2 -and
|
|
(
|
|
($value.StartsWith('"') -and $value.EndsWith('"')) -or
|
|
($value.StartsWith("'") -and $value.EndsWith("'"))
|
|
)
|
|
) {
|
|
$value = $value.Substring(1, $value.Length - 2)
|
|
}
|
|
|
|
$existing = [Environment]::GetEnvironmentVariable($name)
|
|
if ([string]::IsNullOrWhiteSpace($existing)) {
|
|
[Environment]::SetEnvironmentVariable(
|
|
$name,
|
|
$value,
|
|
[EnvironmentVariableTarget]::Process
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-AppcastVersion {
|
|
param([Parameter(Mandatory = $true)][string]$Path)
|
|
|
|
[xml]$xml = Get-Content -LiteralPath $Path -Raw -Encoding UTF8
|
|
$enclosure = $xml.rss.channel.item.enclosure
|
|
if ($null -eq $enclosure) {
|
|
throw "AppCast does not contain channel/item/enclosure: $Path"
|
|
}
|
|
|
|
return $enclosure.GetAttribute('version', $script:SparkleNamespace)
|
|
}
|
|
|
|
function Write-Appcast {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$Path,
|
|
[Parameter(Mandatory = $true)][string]$Version,
|
|
[Parameter(Mandatory = $true)][string]$BuildVersion,
|
|
[Parameter(Mandatory = $true)][string]$Notes,
|
|
[Parameter(Mandatory = $true)][string]$InstallerUrl,
|
|
[Parameter(Mandatory = $true)][string]$Signature,
|
|
[Parameter(Mandatory = $true)][long]$Length
|
|
)
|
|
|
|
$settings = New-Object System.Xml.XmlWriterSettings
|
|
$settings.Encoding = New-Object System.Text.UTF8Encoding($false)
|
|
$settings.Indent = $true
|
|
$settings.IndentChars = ' '
|
|
$settings.NewLineChars = "`n"
|
|
$settings.NewLineHandling = [System.Xml.NewLineHandling]::Replace
|
|
|
|
$writer = [System.Xml.XmlWriter]::Create($Path, $settings)
|
|
try {
|
|
$writer.WriteStartDocument()
|
|
$writer.WriteStartElement('rss')
|
|
$writer.WriteAttributeString('version', '2.0')
|
|
$writer.WriteAttributeString(
|
|
'xmlns',
|
|
'sparkle',
|
|
'http://www.w3.org/2000/xmlns/',
|
|
$script:SparkleNamespace
|
|
)
|
|
$writer.WriteStartElement('channel')
|
|
$writer.WriteElementString('title', 'MoonWell Launcher')
|
|
$writer.WriteElementString(
|
|
'description',
|
|
'MoonWell Launcher updates'
|
|
)
|
|
$writer.WriteElementString('language', 'ru')
|
|
$writer.WriteStartElement('item')
|
|
$writer.WriteElementString(
|
|
'title',
|
|
"MoonWell Launcher $Version"
|
|
)
|
|
$writer.WriteElementString('description', $Notes)
|
|
$writer.WriteElementString(
|
|
'pubDate',
|
|
[DateTimeOffset]::Now.ToString(
|
|
'ddd, dd MMM yyyy HH:mm:ss zzz',
|
|
[Globalization.CultureInfo]::InvariantCulture
|
|
)
|
|
)
|
|
$writer.WriteStartElement('enclosure')
|
|
$writer.WriteAttributeString('url', $InstallerUrl)
|
|
$writer.WriteAttributeString(
|
|
'sparkle',
|
|
'version',
|
|
$script:SparkleNamespace,
|
|
$BuildVersion
|
|
)
|
|
$writer.WriteAttributeString(
|
|
'sparkle',
|
|
'shortVersionString',
|
|
$script:SparkleNamespace,
|
|
$Version
|
|
)
|
|
$writer.WriteAttributeString(
|
|
'sparkle',
|
|
'os',
|
|
$script:SparkleNamespace,
|
|
'windows'
|
|
)
|
|
$writer.WriteAttributeString(
|
|
'sparkle',
|
|
'dsaSignature',
|
|
$script:SparkleNamespace,
|
|
$Signature
|
|
)
|
|
$writer.WriteAttributeString('length', $Length.ToString())
|
|
$writer.WriteAttributeString(
|
|
'type',
|
|
'application/octet-stream'
|
|
)
|
|
$writer.WriteEndElement()
|
|
$writer.WriteEndElement()
|
|
$writer.WriteEndElement()
|
|
$writer.WriteEndElement()
|
|
$writer.WriteEndDocument()
|
|
}
|
|
finally {
|
|
$writer.Dispose()
|
|
}
|
|
}
|
|
|
|
function Assert-SigningKeyPair {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$OpenSsl,
|
|
[Parameter(Mandatory = $true)][string]$PrivateKey,
|
|
[Parameter(Mandatory = $true)][string]$PublicKey,
|
|
[Parameter(Mandatory = $true)][string]$TemporaryPublicKey
|
|
)
|
|
|
|
Invoke-RequiredCommand -Command $OpenSsl -Arguments @(
|
|
'dsa',
|
|
'-in',
|
|
$PrivateKey,
|
|
'-pubout',
|
|
'-out',
|
|
$TemporaryPublicKey
|
|
)
|
|
|
|
$expected = (
|
|
Get-Content -LiteralPath $PublicKey -Raw
|
|
) -replace '\s', ''
|
|
$actual = (
|
|
Get-Content -LiteralPath $TemporaryPublicKey -Raw
|
|
) -replace '\s', ''
|
|
|
|
if ($expected -cne $actual) {
|
|
throw 'The private DSA key does not match the launcher public key.'
|
|
}
|
|
}
|
|
|
|
function Assert-InstallerSignature {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$OpenSsl,
|
|
[Parameter(Mandatory = $true)][string]$InstallerPath,
|
|
[Parameter(Mandatory = $true)][string]$PublicKey,
|
|
[Parameter(Mandatory = $true)][string]$Signature,
|
|
[Parameter(Mandatory = $true)][string]$TemporaryDirectory
|
|
)
|
|
|
|
$signaturePath = Join-Path $TemporaryDirectory 'installer.sig'
|
|
$firstDigestPath = Join-Path $TemporaryDirectory 'installer.sha1'
|
|
[IO.File]::WriteAllBytes(
|
|
$signaturePath,
|
|
[Convert]::FromBase64String($Signature)
|
|
)
|
|
|
|
$stream = [IO.File]::OpenRead($InstallerPath)
|
|
try {
|
|
$sha1 = [Security.Cryptography.SHA1]::Create()
|
|
try {
|
|
[IO.File]::WriteAllBytes(
|
|
$firstDigestPath,
|
|
$sha1.ComputeHash($stream)
|
|
)
|
|
}
|
|
finally {
|
|
$sha1.Dispose()
|
|
}
|
|
}
|
|
finally {
|
|
$stream.Dispose()
|
|
}
|
|
|
|
Invoke-RequiredCommand -Command $OpenSsl -Arguments @(
|
|
'dgst',
|
|
'-sha1',
|
|
'-verify',
|
|
$PublicKey,
|
|
'-signature',
|
|
$signaturePath,
|
|
$firstDigestPath
|
|
)
|
|
}
|
|
|
|
$repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
|
$previousLocation = Get-Location
|
|
|
|
try {
|
|
Set-Location $repositoryRoot
|
|
|
|
$resolvedEnvFile = if ([IO.Path]::IsPathRooted($EnvFile)) {
|
|
$EnvFile
|
|
}
|
|
else {
|
|
Join-Path $repositoryRoot $EnvFile
|
|
}
|
|
Import-DotEnv -Path $resolvedEnvFile
|
|
|
|
if ([string]::IsNullOrWhiteSpace($S3Endpoint)) {
|
|
$S3Endpoint = $env:AWS_ENDPOINT
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($S3Endpoint)) {
|
|
$S3Endpoint = 'https://storage.yandexcloud.net'
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($S3Bucket)) {
|
|
$S3Bucket = $env:AWS_BUCKET
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($S3Bucket)) {
|
|
$S3Bucket = 'warcraft-client'
|
|
}
|
|
|
|
foreach ($url in @($ApiBaseUrl, $AppcastUrl, $S3Endpoint)) {
|
|
$uri = $null
|
|
if (
|
|
-not [Uri]::TryCreate(
|
|
$url,
|
|
[UriKind]::Absolute,
|
|
[ref]$uri
|
|
) -or
|
|
$uri.Scheme -ne 'https'
|
|
) {
|
|
throw "Production URL must be absolute HTTPS: $url"
|
|
}
|
|
}
|
|
|
|
$python = $null
|
|
if (-not $DryRun) {
|
|
foreach ($name in @(
|
|
'AWS_ACCESS_KEY_ID',
|
|
'AWS_SECRET_ACCESS_KEY',
|
|
'LAUNCHER_AUTH_KEY'
|
|
)) {
|
|
$value = [Environment]::GetEnvironmentVariable($name)
|
|
if ([string]::IsNullOrWhiteSpace($value)) {
|
|
throw "Required environment variable $name is not set."
|
|
}
|
|
}
|
|
|
|
$python = Resolve-RequiredCommand `
|
|
-Command 'python' `
|
|
-Description 'Python'
|
|
Invoke-RequiredCommand -Command $python -Arguments @(
|
|
'-c',
|
|
'import boto3'
|
|
)
|
|
}
|
|
|
|
$versionMatch = [regex]::Match(
|
|
(Get-Content -LiteralPath 'pubspec.yaml' -Raw),
|
|
'(?m)^version:\s*(\d+\.\d+\.\d+)\+(\d+)\s*$'
|
|
)
|
|
if (-not $versionMatch.Success) {
|
|
throw 'The pubspec.yaml version must use the X.Y.Z+N format.'
|
|
}
|
|
|
|
$version = $versionMatch.Groups[1].Value
|
|
$buildNumber = $versionMatch.Groups[2].Value
|
|
if (
|
|
-not [string]::IsNullOrWhiteSpace($ExpectedVersion) -and
|
|
$ExpectedVersion -ne $version
|
|
) {
|
|
throw "Expected version $ExpectedVersion, pubspec.yaml contains $version."
|
|
}
|
|
|
|
$privateKey = (Resolve-Path -LiteralPath $PrivateKeyPath).Path
|
|
$publicKey = (Resolve-Path -LiteralPath $PublicKeyPath).Path
|
|
$flutter = Resolve-RequiredCommand `
|
|
-Command $FlutterCommand `
|
|
-Description 'Flutter SDK'
|
|
$dart = Resolve-RequiredCommand `
|
|
-Command $DartCommand `
|
|
-Description 'Dart SDK'
|
|
|
|
if ([string]::IsNullOrWhiteSpace($InnoSetupCommand)) {
|
|
$InnoSetupCommand =
|
|
'C:\Program Files (x86)\Inno Setup 6\ISCC.exe'
|
|
}
|
|
$innoSetup = Resolve-RequiredCommand `
|
|
-Command $InnoSetupCommand `
|
|
-Description 'Inno Setup Compiler'
|
|
|
|
if ([string]::IsNullOrWhiteSpace($OpenSslCommand)) {
|
|
$gitOpenSsl = 'C:\Program Files\Git\usr\bin\openssl.exe'
|
|
$OpenSslCommand = if (Test-Path -LiteralPath $gitOpenSsl) {
|
|
$gitOpenSsl
|
|
}
|
|
else {
|
|
'openssl'
|
|
}
|
|
}
|
|
$openSsl = Resolve-RequiredCommand `
|
|
-Command $OpenSslCommand `
|
|
-Description 'OpenSSL'
|
|
|
|
$temporaryDirectory = Join-Path $repositoryRoot 'build\launcher_release'
|
|
New-Item -ItemType Directory -Force -Path $temporaryDirectory | Out-Null
|
|
|
|
Write-Step "Checking DSA keys for version $version+$buildNumber"
|
|
Assert-SigningKeyPair `
|
|
-OpenSsl $openSsl `
|
|
-PrivateKey $privateKey `
|
|
-PublicKey $publicKey `
|
|
-TemporaryPublicKey (
|
|
Join-Path $temporaryDirectory 'derived_dsa_pub.pem'
|
|
)
|
|
|
|
if (-not $DryRun -and -not $Force) {
|
|
Write-Step 'Checking the published version'
|
|
try {
|
|
$publishedPath = Join-Path $temporaryDirectory 'current_appcast.xml'
|
|
Invoke-WebRequest `
|
|
-Uri $AppcastUrl `
|
|
-UseBasicParsing `
|
|
-OutFile $publishedPath
|
|
$publishedVersion = Get-AppcastVersion -Path $publishedPath
|
|
$publishedReleaseVersion = $publishedVersion.Split('+')[0]
|
|
if (
|
|
-not [string]::IsNullOrWhiteSpace($publishedVersion) -and
|
|
[version]$version -le [version]$publishedReleaseVersion
|
|
) {
|
|
throw (
|
|
"Version $version is not newer than published version " +
|
|
"$publishedVersion. Increase X.Y.Z or use -Force."
|
|
)
|
|
}
|
|
}
|
|
catch {
|
|
if ($_.Exception.Message -like 'Version *') {
|
|
throw
|
|
}
|
|
Write-Warning (
|
|
'Could not determine the currently published version: ' +
|
|
$_.Exception.Message
|
|
)
|
|
}
|
|
}
|
|
|
|
if (-not $SkipChecks) {
|
|
Write-Step 'Dependencies, formatting, analysis, and tests'
|
|
Invoke-RequiredCommand -Command $flutter -Arguments @('pub', 'get')
|
|
Invoke-RequiredCommand -Command $dart -Arguments @(
|
|
'format',
|
|
'--output=none',
|
|
'--set-exit-if-changed',
|
|
'lib',
|
|
'test'
|
|
)
|
|
Invoke-RequiredCommand -Command $flutter -Arguments @(
|
|
'analyze',
|
|
'lib',
|
|
'test'
|
|
)
|
|
Invoke-RequiredCommand -Command $flutter -Arguments @('test')
|
|
}
|
|
|
|
Write-Step 'Building the Windows production application'
|
|
Invoke-RequiredCommand -Command $flutter -Arguments @(
|
|
'build',
|
|
'windows',
|
|
'--release',
|
|
"--dart-define=MOONWELL_API_BASE_URL=$ApiBaseUrl",
|
|
"--dart-define=MOONWELL_APPCAST_URL=$AppcastUrl"
|
|
)
|
|
|
|
Write-Step 'Building the Inno Setup installer'
|
|
Invoke-RequiredCommand -Command $innoSetup -Arguments @(
|
|
"/DMyAppVersion=$version",
|
|
'installer\moonwell_launcher.iss'
|
|
)
|
|
|
|
$installerPath = Join-Path (
|
|
Join-Path $repositoryRoot 'build\installer'
|
|
) "moonwell_launcher_${version}_windows_setup.exe"
|
|
if (-not (Test-Path -LiteralPath $installerPath -PathType Leaf)) {
|
|
throw "The installer was not created: $installerPath"
|
|
}
|
|
|
|
Write-Step 'Signing the installer with DSA'
|
|
$gitOpenSslDirectory = Split-Path $openSsl -Parent
|
|
$previousPath = $env:PATH
|
|
try {
|
|
$env:PATH = "$gitOpenSslDirectory;$env:PATH"
|
|
$signOutput = & $dart run auto_updater:sign_update `
|
|
$installerPath `
|
|
$privateKey 2>&1
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Could not sign the installer: $signOutput"
|
|
}
|
|
}
|
|
finally {
|
|
$env:PATH = $previousPath
|
|
}
|
|
|
|
$signatureMatch = [regex]::Match(
|
|
($signOutput -join "`n"),
|
|
'sparkle:dsaSignature="([^"]+)"',
|
|
[Text.RegularExpressions.RegexOptions]::Singleline
|
|
)
|
|
if (-not $signatureMatch.Success) {
|
|
throw 'The signing tool did not return sparkle:dsaSignature.'
|
|
}
|
|
$signature = $signatureMatch.Groups[1].Value -replace '\s', ''
|
|
[void][Convert]::FromBase64String($signature)
|
|
|
|
$installer = Get-Item -LiteralPath $installerPath
|
|
$installerSha256 = Get-FileSha256 -Path $installerPath
|
|
Assert-InstallerSignature `
|
|
-OpenSsl $openSsl `
|
|
-InstallerPath $installerPath `
|
|
-PublicKey $publicKey `
|
|
-Signature $signature `
|
|
-TemporaryDirectory $temporaryDirectory
|
|
|
|
$installerUrl = (
|
|
$S3Endpoint.TrimEnd('/') +
|
|
'/' +
|
|
$S3Bucket +
|
|
'/' +
|
|
$S3Key
|
|
)
|
|
$appcastBuildVersion = "$version+$buildNumber"
|
|
$appcastPath = if ($DryRun) {
|
|
Join-Path $temporaryDirectory 'appcast.xml'
|
|
}
|
|
else {
|
|
Join-Path $repositoryRoot 'appcast.xml'
|
|
}
|
|
Write-Step 'Generating appcast.xml'
|
|
Write-Appcast `
|
|
-Path $appcastPath `
|
|
-Version $version `
|
|
-BuildVersion $appcastBuildVersion `
|
|
-Notes $ReleaseNotes `
|
|
-InstallerUrl $installerUrl `
|
|
-Signature $signature `
|
|
-Length $installer.Length
|
|
|
|
[xml]$generatedAppcast = Get-Content `
|
|
-LiteralPath $appcastPath `
|
|
-Raw `
|
|
-Encoding UTF8
|
|
if ((Get-AppcastVersion -Path $appcastPath) -ne $appcastBuildVersion) {
|
|
throw 'The generated AppCast contains the wrong version.'
|
|
}
|
|
|
|
if ($DryRun) {
|
|
Write-Step 'Dry run complete - production was not changed'
|
|
Write-Host "Version: $version+$buildNumber"
|
|
Write-Host "Installer: $installerPath"
|
|
Write-Host "Size: $($installer.Length)"
|
|
Write-Host "SHA-256: $installerSha256"
|
|
Write-Host "AppCast: $appcastPath"
|
|
return
|
|
}
|
|
|
|
Write-Step "Uploading $S3Bucket/$S3Key"
|
|
$env:MOONWELL_RELEASE_FILE = $installerPath
|
|
$env:MOONWELL_RELEASE_ENDPOINT = $S3Endpoint
|
|
$env:MOONWELL_RELEASE_BUCKET = $S3Bucket
|
|
$env:MOONWELL_RELEASE_KEY = $S3Key
|
|
$env:MOONWELL_RELEASE_VERSION = $version
|
|
$env:MOONWELL_RELEASE_SHA256 = $installerSha256
|
|
$uploadHelper = Join-Path `
|
|
$repositoryRoot `
|
|
'tool\upload_launcher_release.py'
|
|
if (-not (Test-Path -LiteralPath $uploadHelper -PathType Leaf)) {
|
|
throw "S3 upload helper was not found: $uploadHelper"
|
|
}
|
|
|
|
try {
|
|
Invoke-RequiredCommand -Command $python -Arguments @($uploadHelper)
|
|
}
|
|
finally {
|
|
Remove-Item Env:MOONWELL_RELEASE_FILE -ErrorAction SilentlyContinue
|
|
Remove-Item Env:MOONWELL_RELEASE_ENDPOINT -ErrorAction SilentlyContinue
|
|
Remove-Item Env:MOONWELL_RELEASE_BUCKET -ErrorAction SilentlyContinue
|
|
Remove-Item Env:MOONWELL_RELEASE_KEY -ErrorAction SilentlyContinue
|
|
Remove-Item Env:MOONWELL_RELEASE_VERSION -ErrorAction SilentlyContinue
|
|
Remove-Item Env:MOONWELL_RELEASE_SHA256 -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
Write-Step 'Verifying the uploaded installer'
|
|
$downloadedInstaller = Join-Path `
|
|
$temporaryDirectory `
|
|
'published_installer.exe'
|
|
Invoke-WebRequest `
|
|
-Uri $installerUrl `
|
|
-UseBasicParsing `
|
|
-OutFile $downloadedInstaller
|
|
$publishedSha256 = Get-FileSha256 -Path $downloadedInstaller
|
|
if ($publishedSha256 -ne $installerSha256) {
|
|
throw (
|
|
'The uploaded installer SHA-256 does not match the local file: ' +
|
|
"$publishedSha256"
|
|
)
|
|
}
|
|
|
|
Write-Step 'Publishing AppCast'
|
|
$publishResponse = Invoke-WebRequest `
|
|
-Uri "$($ApiBaseUrl.TrimEnd('/'))/api/service/update-appcast" `
|
|
-Method Post `
|
|
-Headers @{
|
|
Auth = $env:LAUNCHER_AUTH_KEY
|
|
Accept = 'application/json'
|
|
} `
|
|
-ContentType 'application/xml' `
|
|
-Body ([IO.File]::ReadAllBytes($appcastPath)) `
|
|
-UseBasicParsing
|
|
if ($publishResponse.StatusCode -lt 200 -or $publishResponse.StatusCode -ge 300) {
|
|
throw "AppCast publication returned HTTP $($publishResponse.StatusCode)."
|
|
}
|
|
|
|
Write-Step 'Verifying the published AppCast'
|
|
$publishedAppcastPath = Join-Path `
|
|
$temporaryDirectory `
|
|
'published_appcast.xml'
|
|
$publicResponse = Invoke-WebRequest `
|
|
-Uri $AppcastUrl `
|
|
-UseBasicParsing `
|
|
-OutFile $publishedAppcastPath `
|
|
-PassThru
|
|
if ($publicResponse.StatusCode -ne 200) {
|
|
throw "The public AppCast returned HTTP $($publicResponse.StatusCode)."
|
|
}
|
|
if (
|
|
(Get-FileSha256 -Path $publishedAppcastPath) -ne
|
|
(Get-FileSha256 -Path $appcastPath)
|
|
) {
|
|
throw 'The published AppCast does not match the local file.'
|
|
}
|
|
|
|
[xml]$publishedAppcast = Get-Content `
|
|
-LiteralPath $publishedAppcastPath `
|
|
-Raw `
|
|
-Encoding UTF8
|
|
$publishedEnclosure = $publishedAppcast.rss.channel.item.enclosure
|
|
if (
|
|
$publishedEnclosure.GetAttribute(
|
|
'version',
|
|
$script:SparkleNamespace
|
|
) -ne $appcastBuildVersion -or
|
|
[long]$publishedEnclosure.length -ne $installer.Length -or
|
|
$publishedEnclosure.url -ne $installerUrl
|
|
) {
|
|
throw 'The published AppCast contains incorrect release data.'
|
|
}
|
|
|
|
Write-Step 'Production deployment complete'
|
|
Write-Host "Version: $version+$buildNumber"
|
|
Write-Host "Installer: $installerUrl"
|
|
Write-Host "SHA-256: $installerSha256"
|
|
Write-Host "AppCast: $AppcastUrl"
|
|
}
|
|
finally {
|
|
Set-Location $previousLocation
|
|
}
|