diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8cab22..ae9b06e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -691,6 +691,10 @@ jobs: if: matrix.channel == 'install.ps1' shell: pwsh run: ./scripts/test-install.ps1 + - name: Run install.ps1 production R2 smoke + if: matrix.channel == 'install.ps1' + shell: pwsh + run: ./scripts/test-install-r2.ps1 - name: Run Homebrew channel validation if: matrix.channel == 'Homebrew' run: python scripts/test-distribution-homebrew.py diff --git a/docs/release.md b/docs/release.md index c749676..eb89d68 100644 --- a/docs/release.md +++ b/docs/release.md @@ -115,16 +115,16 @@ already exposes. Pinned installs use immutable versioned objects: ```text -https://releases.wrightkit.dev/releases//wright--. -https://releases.wrightkit.dev/releases//wright--..sha256 +https://releases.wrightkit.dev/wright/releases//wright--. +https://releases.wrightkit.dev/wright/releases//wright--..sha256 ``` -Latest installs first read `https://releases.wrightkit.dev/latest/version`, +Latest installs first read `https://releases.wrightkit.dev/wright/latest/version`, then download the corresponding version-named archive and checksum from -`/latest/`. The release workflow uploads and publicly verifies every versioned -and latest archive/checksum pair before writing that `latest/version` pointer, -so the installer cannot resolve a new version before its complete artifact set -is available. `latest/version` uses `Cache-Control: no-store`; all archive and +`/releases//`. The release workflow publicly verifies every versioned +archive/checksum pair before writing that `latest/version` pointer, so the +installer cannot resolve a new version before its complete artifact set is +available. `latest/version` uses `Cache-Control: no-store`; all archive and checksum paths are version-named and use long-lived immutable caching. This avoids stale latest pointers without a separate Worker, API, or GitHub Releases API lookup. @@ -221,8 +221,8 @@ Configure these optional/required environment secrets: ## Supported installation channels All channels consume canonical released archives and none of them rebuild -Wright. `install.sh` consumes the R2 copies described above; the package -managers and Windows installer continue to consume GitHub Release archives. +Wright. `install.sh` and `install.ps1` consume the R2 copies described above; +the package managers continue to consume GitHub Release archives. Metadata lives under [`dist/`](dist/README.md), generated by `scripts/update-dist-manifests.py`, and is regenerated by the release PR maintenance step and again by the `package-manifests` job from the published per-target diff --git a/install.ps1 b/install.ps1 index da05931..1cb0b73 100644 --- a/install.ps1 +++ b/install.ps1 @@ -2,8 +2,7 @@ param( [string]$Version, [string]$InstallDir, - [string]$BaseUrl, - [string]$ApiUrl + [string]$BaseUrl ) $ErrorActionPreference = "Stop" @@ -20,19 +19,141 @@ function Resolve-Setting([string]$Value, [string]$EnvironmentName, [string]$Defa return $Default } -function Get-Version([string]$RequestedVersion, [string]$ReleaseApiUrl) { +if (-not ("Wright.Http2Downloader" -as [type])) { + Add-Type -TypeDefinition @' +using System; +using System.ComponentModel; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace Wright { + public static class Http2Downloader { + const uint WINHTTP_ACCESS_TYPE_DEFAULT_PROXY = 0; + const uint WINHTTP_FLAG_SECURE = 0x00800000; + const uint WINHTTP_OPTION_ENABLE_HTTP_PROTOCOL = 133; + const uint WINHTTP_OPTION_HTTP_PROTOCOL_USED = 134; + const uint WINHTTP_OPTION_HTTP_PROTOCOL_REQUIRED = 145; + const uint WINHTTP_PROTOCOL_FLAG_HTTP2 = 1; + const uint WINHTTP_QUERY_STATUS_CODE = 19; + const int ERROR_INSUFFICIENT_BUFFER = 122; + + [DllImport("winhttp.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern IntPtr WinHttpOpen(string agent, uint accessType, IntPtr proxy, IntPtr bypass, uint flags); + [DllImport("winhttp.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern IntPtr WinHttpConnect(IntPtr session, string server, ushort port, uint reserved); + [DllImport("winhttp.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern IntPtr WinHttpOpenRequest(IntPtr connection, string verb, string objectName, string version, string referrer, IntPtr acceptTypes, uint flags); + [DllImport("winhttp.dll", SetLastError = true)] + static extern bool WinHttpSetOption(IntPtr handle, uint option, ref uint buffer, uint bufferLength); + [DllImport("winhttp.dll", SetLastError = true)] + static extern bool WinHttpQueryOption(IntPtr handle, uint option, out uint buffer, ref uint bufferLength); + [DllImport("winhttp.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern bool WinHttpQueryHeaders(IntPtr request, uint infoLevel, string name, StringBuilder buffer, ref uint bufferLength, IntPtr index); + [DllImport("winhttp.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern bool WinHttpSendRequest(IntPtr request, string headers, uint headersLength, IntPtr optional, uint optionalLength, uint totalLength, IntPtr context); + [DllImport("winhttp.dll", SetLastError = true)] + static extern bool WinHttpReceiveResponse(IntPtr request, IntPtr reserved); + [DllImport("winhttp.dll", SetLastError = true)] + static extern bool WinHttpQueryDataAvailable(IntPtr request, out uint available); + [DllImport("winhttp.dll", SetLastError = true)] + static extern bool WinHttpReadData(IntPtr request, [Out] byte[] buffer, uint bytesToRead, out uint bytesRead); + [DllImport("winhttp.dll", SetLastError = true)] + static extern bool WinHttpCloseHandle(IntPtr handle); + + static void Check(bool success, string operation) { + if (!success) throw new Win32Exception(Marshal.GetLastWin32Error(), operation); + } + + static uint StatusCode(IntPtr request) { + uint length = 0; + WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE, null, null, ref length, IntPtr.Zero); + if (Marshal.GetLastWin32Error() != ERROR_INSUFFICIENT_BUFFER) { + throw new Win32Exception(Marshal.GetLastWin32Error(), "WinHttpQueryHeaders"); + } + var value = new StringBuilder((int)(length / 2)); + Check(WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE, null, value, ref length, IntPtr.Zero), "WinHttpQueryHeaders"); + return UInt32.Parse(value.ToString()); + } + + public static void Download(string address, string destination) { + var uri = new Uri(address); + if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps) { + throw new ArgumentException("only HTTP(S) URLs are supported", "address"); + } + IntPtr session = IntPtr.Zero; + IntPtr connection = IntPtr.Zero; + IntPtr request = IntPtr.Zero; + try { + session = WinHttpOpen("wright-installer", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, IntPtr.Zero, IntPtr.Zero, 0); + if (session == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error(), "WinHttpOpen"); + uint http2 = WINHTTP_PROTOCOL_FLAG_HTTP2; + Check(WinHttpSetOption(session, WINHTTP_OPTION_ENABLE_HTTP_PROTOCOL, ref http2, sizeof(uint)), "WinHttpSetOption(HTTP/2)"); + connection = WinHttpConnect(session, uri.Host, (ushort)uri.Port, 0); + if (connection == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error(), "WinHttpConnect"); + uint flags = uri.Scheme == Uri.UriSchemeHttps ? WINHTTP_FLAG_SECURE : 0; + request = WinHttpOpenRequest(connection, "GET", uri.PathAndQuery, null, null, IntPtr.Zero, flags); + if (request == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error(), "WinHttpOpenRequest"); + if (uri.Scheme == Uri.UriSchemeHttps) { + Check(WinHttpSetOption(request, WINHTTP_OPTION_HTTP_PROTOCOL_REQUIRED, ref http2, sizeof(uint)), "WinHttpSetOption(require HTTP/2)"); + } + Check(WinHttpSendRequest(request, null, 0, IntPtr.Zero, 0, 0, IntPtr.Zero), "WinHttpSendRequest"); + Check(WinHttpReceiveResponse(request, IntPtr.Zero), "WinHttpReceiveResponse"); + if (uri.Scheme == Uri.UriSchemeHttps) { + uint length = sizeof(uint); + uint used; + Check(WinHttpQueryOption(request, WINHTTP_OPTION_HTTP_PROTOCOL_USED, out used, ref length), "WinHttpQueryOption(HTTP protocol)"); + if (used != WINHTTP_PROTOCOL_FLAG_HTTP2) throw new InvalidOperationException("WinHTTP did not negotiate HTTP/2"); + } + uint status = StatusCode(request); + if (status < 200 || status >= 300) throw new InvalidOperationException("HTTP status " + status); + using (var output = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None)) { + while (true) { + uint available; + Check(WinHttpQueryDataAvailable(request, out available), "WinHttpQueryDataAvailable"); + if (available == 0) break; + var buffer = new byte[(int)available]; + uint read; + Check(WinHttpReadData(request, buffer, available, out read), "WinHttpReadData"); + if (read == 0) throw new InvalidOperationException("WinHttpReadData returned no data"); + output.Write(buffer, 0, (int)read); + } + } + } finally { + if (request != IntPtr.Zero) WinHttpCloseHandle(request); + if (connection != IntPtr.Zero) WinHttpCloseHandle(connection); + if (session != IntPtr.Zero) WinHttpCloseHandle(session); + } + } + } +} +'@ +} + +function Get-RemoteFile([string]$Uri, [string]$Destination) { + [Wright.Http2Downloader]::Download($Uri, $Destination) +} + +function Get-Version([string]$RequestedVersion, [string]$ReleaseBaseUrl) { if ($RequestedVersion) { $resolved = $RequestedVersion.TrimStart("v") } else { + $latestVersionUrl = "$($ReleaseBaseUrl.TrimEnd('/'))/latest/version" + $latestVersionPath = Join-Path ([IO.Path]::GetTempPath()) ("wright-latest-" + [Guid]::NewGuid().ToString("N")) try { - $release = Invoke-RestMethod -Uri $ReleaseApiUrl -Headers @{ "User-Agent" = "wright-installer" } - $resolved = ([string]$release.tag_name).TrimStart("v") + Get-RemoteFile $latestVersionUrl $latestVersionPath + $content = Get-Content -LiteralPath $latestVersionPath -Raw + $resolved = $content.Trim().TrimStart("v") if (-not $resolved) { - Fail "latest release response from $ReleaseApiUrl did not contain a tag; pin a version with -Version" + Fail "latest version response from $latestVersionUrl was empty; pin a version with -Version" } } catch { - if ($_.Exception.Message -like "error: latest release response*") { throw } - Fail "could not resolve the latest release from $ReleaseApiUrl; pin a version with -Version" + if ($_.Exception.Message -like "error: latest version response*") { throw } + Fail "could not resolve the latest release from ${latestVersionUrl}: $($_.Exception.Message); pin a version with -Version" + } finally { + if (Test-Path -LiteralPath $latestVersionPath) { + Remove-Item -LiteralPath $latestVersionPath -Force -ErrorAction SilentlyContinue + } } } if ($resolved -notmatch '^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$') { @@ -70,9 +191,8 @@ if ([Runtime.InteropServices.RuntimeInformation]::OSArchitecture -ne [Runtime.In Fail "unsupported CPU architecture; install.ps1 supports Windows x86_64 only" } -$BaseUrl = Resolve-Setting $BaseUrl "WRIGHT_INSTALL_BASE_URL" "https://github.com/wrightkit/wright/releases/download" -$ApiUrl = Resolve-Setting $ApiUrl "WRIGHT_API_URL" "https://api.github.com/repos/wrightkit/wright/releases/latest" -$Version = Get-Version $Version $ApiUrl +$BaseUrl = Resolve-Setting $BaseUrl "WRIGHT_INSTALL_BASE_URL" "https://releases.wrightkit.dev/wright" +$Version = Get-Version $Version $BaseUrl if (-not $InstallDir) { $InstallRoot = if ($env:LOCALAPPDATA) { $env:LOCALAPPDATA } else { $env:USERPROFILE } if (-not $InstallRoot) { Fail "could not determine a user-writable install directory; pass -InstallDir" } @@ -80,7 +200,7 @@ if (-not $InstallDir) { } $ArchiveName = "wright-$Version-$Target.zip" -$ArchiveUrl = "$($BaseUrl.TrimEnd('/'))/v$Version/$ArchiveName" +$ArchiveUrl = "$($BaseUrl.TrimEnd('/'))/releases/$Version/$ArchiveName" $ChecksumUrl = "${ArchiveUrl}.sha256" $TempRoot = Join-Path ([IO.Path]::GetTempPath()) ("wright-install-" + [Guid]::NewGuid().ToString("N")) $ExtractDir = Join-Path $TempRoot "extract" @@ -92,10 +212,10 @@ try { $ChecksumPath = "$ArchivePath.sha256" Write-Host "==> downloading $ArchiveUrl" try { - Invoke-WebRequest -Uri $ArchiveUrl -OutFile $ArchivePath -UseBasicParsing - Invoke-WebRequest -Uri $ChecksumUrl -OutFile $ChecksumPath -UseBasicParsing + Get-RemoteFile $ArchiveUrl $ArchivePath + Get-RemoteFile $ChecksumUrl $ChecksumPath } catch { - Fail "failed to download the release archive or checksum for v$Version from $BaseUrl" + Fail "failed to download the release archive or checksum for v$Version from ${BaseUrl}: $($_.Exception.Message)" } Write-Host "==> verifying SHA-256 checksum" diff --git a/install.sh b/install.sh index 2a512ed..c0d52df 100755 --- a/install.sh +++ b/install.sh @@ -19,12 +19,11 @@ set -euo pipefail -WRIGHT_INSTALL_BASE_URL="${WRIGHT_INSTALL_BASE_URL:-https://releases.wrightkit.dev}" +WRIGHT_INSTALL_BASE_URL="${WRIGHT_INSTALL_BASE_URL:-https://releases.wrightkit.dev/wright}" VERSION="" INSTALL_DIR="" TMP_DIR="" -VERSION_FROM_LATEST=false usage() { sed -n '2,11p' "$0" | sed 's/^# \?//' @@ -114,7 +113,6 @@ fi if [[ -z "$VERSION" ]]; then echo "==> resolving latest stable release" - VERSION_FROM_LATEST=true VERSION="$(curl -fsSL "$WRIGHT_INSTALL_BASE_URL/latest/version" 2>/dev/null)" \ || fail "could not resolve the latest release from $WRIGHT_INSTALL_BASE_URL/latest/version; pin a version with --version" VERSION="$(printf '%s' "$VERSION" | tr -d '[:space:]')" @@ -125,11 +123,7 @@ if [[ -z "$VERSION" ]]; then fi ARCHIVE="wright-$VERSION-$TARGET.tar.gz" -if [[ "$VERSION_FROM_LATEST" == true ]]; then - ARCHIVE_URL="$WRIGHT_INSTALL_BASE_URL/latest/$ARCHIVE" -else - ARCHIVE_URL="$WRIGHT_INSTALL_BASE_URL/releases/$VERSION/$ARCHIVE" -fi +ARCHIVE_URL="$WRIGHT_INSTALL_BASE_URL/releases/$VERSION/$ARCHIVE" CHECKSUM_URL="$ARCHIVE_URL.sha256" EXPECTED_DIR="wright-$VERSION-$TARGET" diff --git a/scripts/test-install-r2.ps1 b/scripts/test-install-r2.ps1 new file mode 100644 index 0000000..7188a3d --- /dev/null +++ b/scripts/test-install-r2.ps1 @@ -0,0 +1,34 @@ +$ErrorActionPreference = "Stop" +$Root = Split-Path -Parent $PSScriptRoot +$Installer = Join-Path $Root "install.ps1" +$Work = Join-Path ([IO.Path]::GetTempPath()) ("wright-install-r2-test-" + [Guid]::NewGuid().ToString("N")) +$InstallDir = Join-Path $Work "bin" + +function Fail([string]$Message) { + throw "FAIL: $Message" +} + +try { + & $Installer -InstallDir $InstallDir + $ExpectedExecutables = @("wright.exe", "wright-lsp.exe") + if ($ExpectedExecutables.Count -ne 2 -or + $ExpectedExecutables[0] -ne "wright.exe" -or + $ExpectedExecutables[1] -ne "wright-lsp.exe") { + Fail "production R2 smoke must cover wright.exe and wright-lsp.exe" + } + foreach ($Name in $ExpectedExecutables) { + $Executable = Join-Path $InstallDir $Name + if (-not (Test-Path -LiteralPath $Executable -PathType Leaf)) { + Fail "production R2 install did not install $Name" + } + & $Executable --version | Out-Null + if ($LASTEXITCODE -ne 0) { + Fail "production R2 install smoke failed for $Name" + } + } + Write-Host "PASS: production R2 install and native smoke check" +} finally { + if (Test-Path -LiteralPath $Work) { + Remove-Item -LiteralPath $Work -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/scripts/test-install.ps1 b/scripts/test-install.ps1 index 5034f11..e773c71 100644 --- a/scripts/test-install.ps1 +++ b/scripts/test-install.ps1 @@ -13,9 +13,23 @@ function Fail([string]$Message) { } try { - $Release = Join-Path $Work "v$Version" - $Payload = Join-Path $Release "wright-$Version-$Target" - New-Item -ItemType Directory -Path $Payload -Force | Out-Null + $InstallerText = Get-Content -LiteralPath $Installer -Raw + if ($InstallerText -notmatch 'https://releases\.wrightkit\.dev/wright' -or + $InstallerText -notmatch 'WinHttpSetOption\(session,\s*WINHTTP_OPTION_ENABLE_HTTP_PROTOCOL' -or + $InstallerText -notmatch 'WinHttpSetOption\(request,\s*WINHTTP_OPTION_HTTP_PROTOCOL_REQUIRED' -or + $InstallerText -notmatch 'WinHttpQueryOption\(request,\s*WINHTTP_OPTION_HTTP_PROTOCOL_USED' -or + $InstallerText -match 'Start-BitsTransfer|curl\.exe|Invoke-(WebRequest|RestMethod)') { + Fail "installer must require and verify WinHTTP HTTP/2 for HTTPS release downloads" + } + $DownloadCount = [regex]::Matches($InstallerText, '(?m)^\s*Get-RemoteFile\s+\$').Count + if ($DownloadCount -ne 3) { + Fail "installer must use WinHTTP for latest, archive, and checksum requests" + } + + $VersionedRelease = Join-Path $Work "wright\releases\$Version" + $LatestRelease = Join-Path $Work "wright\latest" + $Payload = Join-Path $Work "payload\wright-$Version-$Target" + New-Item -ItemType Directory -Path $VersionedRelease, $LatestRelease, $Payload -Force | Out-Null foreach ($Name in @("wright.exe", "wright-lsp.exe")) { $Source = Join-Path $Root "target\debug\$Name" if (-not (Test-Path -LiteralPath $Source -PathType Leaf)) { @@ -24,37 +38,28 @@ try { Copy-Item -LiteralPath $Source -Destination (Join-Path $Payload $Name) } $ArchiveName = "wright-$Version-$Target.zip" - $Archive = Join-Path $Release $ArchiveName + $Archive = Join-Path $VersionedRelease $ArchiveName Compress-Archive -LiteralPath $Payload -DestinationPath $Archive -Force $Hash = (Get-FileHash -LiteralPath $Archive -Algorithm SHA256).Hash.ToLowerInvariant() "$Hash $ArchiveName" | Set-Content -LiteralPath "${Archive}.sha256" -NoNewline -Encoding ASCII + $Version | Set-Content -LiteralPath (Join-Path $LatestRelease "version") -NoNewline -Encoding ASCII - $ApiDirectory = Join-Path $Work "repos\wrightkit\wright\releases" - New-Item -ItemType Directory -Path $ApiDirectory -Force | Out-Null - '{"tag_name":"v' + $Version + '","draft":false,"prerelease":false}' | - Set-Content -LiteralPath (Join-Path $ApiDirectory "latest") -NoNewline -Encoding ASCII $ServerCode = @' import http.server import os import sys -class Handler(http.server.SimpleHTTPRequestHandler): - def guess_type(self, path): - if path.endswith("/latest"): - return "application/json" - return super().guess_type(path) - os.chdir(sys.argv[2]) -http.server.ThreadingHTTPServer(("127.0.0.1", int(sys.argv[1])), Handler).serve_forever() +http.server.ThreadingHTTPServer(("127.0.0.1", int(sys.argv[1])), http.server.SimpleHTTPRequestHandler).serve_forever() '@ $ServerScript = Join-Path $Work "server.py" $ServerCode | Set-Content -LiteralPath $ServerScript -NoNewline -Encoding ASCII $Server = Start-Process -FilePath "python" -ArgumentList @($ServerScript, $Port, $Work) -PassThru -WindowStyle Hidden - $BaseUrl = "http://127.0.0.1:$Port" - $ApiUrl = "$BaseUrl/repos/wrightkit/wright/releases/latest" + $BaseUrl = "http://127.0.0.1:$Port/wright" + $LatestVersionUrl = "$BaseUrl/latest/version" for ($Attempt = 0; $Attempt -lt 30; $Attempt++) { try { - Invoke-RestMethod -Uri $ApiUrl | Out-Null + Invoke-WebRequest -Uri $LatestVersionUrl -UseBasicParsing | Out-Null break } catch { if ($Attempt -eq 29) { Fail "local release server did not become ready" } @@ -65,7 +70,7 @@ http.server.ThreadingHTTPServer(("127.0.0.1", int(sys.argv[1])), Handler).serve_ $UnknownVersion = "0.0.0" $UnknownDir = Join-Path $Work "unknown" try { - & $Installer -Version $UnknownVersion -InstallDir $UnknownDir -BaseUrl $BaseUrl -ApiUrl $ApiUrl + & $Installer -Version $UnknownVersion -InstallDir $UnknownDir -BaseUrl $BaseUrl Fail "unknown exact version was accepted or ignored" } catch { if ($_.Exception.Message -notmatch "failed to download") { throw } @@ -76,7 +81,7 @@ http.server.ThreadingHTTPServer(("127.0.0.1", int(sys.argv[1])), Handler).serve_ Write-Host "PASS: exact version selection" $PinnedDir = Join-Path $Work "pinned" - & $Installer -Version $Version -InstallDir $PinnedDir -BaseUrl $BaseUrl -ApiUrl $ApiUrl + & $Installer -Version $Version -InstallDir $PinnedDir -BaseUrl $BaseUrl if (-not (Test-Path -LiteralPath (Join-Path $PinnedDir "wright.exe")) -or -not (Test-Path -LiteralPath (Join-Path $PinnedDir "wright-lsp.exe"))) { Fail "pinned install did not install both executables" @@ -88,17 +93,17 @@ http.server.ThreadingHTTPServer(("127.0.0.1", int(sys.argv[1])), Handler).serve_ if ($LASTEXITCODE -ne 0) { Fail "native post-install smoke failed" } Write-Host "PASS: pinned install and native smoke check" - $LatestDir = Join-Path $Work "latest" - & $Installer -InstallDir $LatestDir -BaseUrl $BaseUrl -ApiUrl $ApiUrl + $LatestDir = Join-Path $Work "latest-install" + & $Installer -InstallDir $LatestDir -BaseUrl $BaseUrl if (-not (Test-Path -LiteralPath (Join-Path $LatestDir "wright.exe"))) { Fail "latest-release install did not install wright.exe" } - Write-Host "PASS: latest-release resolution" + Write-Host "PASS: latest-release versioned R2 path resolution" "$(('0' * 64) -join '') $ArchiveName" | Set-Content -LiteralPath "${Archive}.sha256" -NoNewline -Encoding ASCII $CorruptDir = Join-Path $Work "corrupt" try { - & $Installer -Version $Version -InstallDir $CorruptDir -BaseUrl $BaseUrl -ApiUrl $ApiUrl + & $Installer -Version $Version -InstallDir $CorruptDir -BaseUrl $BaseUrl Fail "checksum mismatch was accepted" } catch { if ($_.Exception.Message -notmatch "checksum verification failed") { throw } @@ -107,6 +112,7 @@ http.server.ThreadingHTTPServer(("127.0.0.1", int(sys.argv[1])), Handler).serve_ Fail "checksum failure left a partial installation" } Write-Host "PASS: checksum mismatch is rejected before installation" + } finally { if ($Server) { Stop-Process -Id $Server.Id -Force -ErrorAction SilentlyContinue } if (Test-Path -LiteralPath $Work) { Remove-Item -LiteralPath $Work -Recurse -Force -ErrorAction SilentlyContinue } diff --git a/scripts/test-install.sh b/scripts/test-install.sh index f9c9094..1c798a3 100755 --- a/scripts/test-install.sh +++ b/scripts/test-install.sh @@ -64,11 +64,10 @@ wait_for_server() { } for triple in x86_64-unknown-linux-gnu aarch64-apple-darwin x86_64-apple-darwin; do - make_archive "$WORK/mock" "$triple" + make_archive "$WORK/mock/wright" "$triple" done -mkdir -p "$WORK/mock/latest" -cp "$WORK/mock/releases/$VERSION"/* "$WORK/mock/latest/" -printf '%s\n' "$VERSION" > "$WORK/mock/latest/version" +mkdir -p "$WORK/mock/wright/latest" +printf '%s\n' "$VERSION" > "$WORK/mock/wright/latest/version" python3 -m http.server "$PORT" --directory "$WORK/mock" >/dev/null 2>&1 & SERVER_PID=$! @@ -88,8 +87,14 @@ report() { fi } +if grep -Fq 'https://releases.wrightkit.dev/wright' "$INSTALLER"; then + report "namespaced R2 default base" ok +else + report "namespaced R2 default base" fail +fi + run_install() { - WRIGHT_INSTALL_BASE_URL="$BASE_URL" "$INSTALLER" "$@" + WRIGHT_INSTALL_BASE_URL="$BASE_URL/wright" "$INSTALLER" "$@" } expect_success() { @@ -124,7 +129,7 @@ expect_success "latest release resolves without the GitHub API" "$WORK/d2" printf 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa %s\n' \ "wright-$VERSION-x86_64-unknown-linux-gnu.tar.gz" \ - > "$WORK/mock/releases/$VERSION/wright-$VERSION-x86_64-unknown-linux-gnu.tar.gz.sha256" + > "$WORK/mock/wright/releases/$VERSION/wright-$VERSION-x86_64-unknown-linux-gnu.tar.gz.sha256" if WRIGHT_INSTALL_OS=linux WRIGHT_INSTALL_ARCH=x86_64 \ run_install --dir "$WORK/d3" --version "$VERSION" >"$INSTALL_OUTPUT" 2>&1; then report "checksum mismatch is rejected before install" fail @@ -138,7 +143,7 @@ test ! -e "$WORK/d3/wright" \ || report "nothing installed after checksum failure" fail printf 'not-a-hash %s\n' "wright-$VERSION-x86_64-unknown-linux-gnu.tar.gz" \ - > "$WORK/mock/releases/$VERSION/wright-$VERSION-x86_64-unknown-linux-gnu.tar.gz.sha256" + > "$WORK/mock/wright/releases/$VERSION/wright-$VERSION-x86_64-unknown-linux-gnu.tar.gz.sha256" if WRIGHT_INSTALL_OS=linux WRIGHT_INSTALL_ARCH=x86_64 \ run_install --dir "$WORK/d3b" --version "$VERSION" >"$INSTALL_OUTPUT" 2>&1; then report "malformed checksum file is rejected" fail @@ -148,7 +153,7 @@ else || report "malformed checksum file is rejected" fail fi -(cd "$WORK/mock/releases/$VERSION" && shasum -a 256 "wright-$VERSION-x86_64-unknown-linux-gnu.tar.gz" \ +(cd "$WORK/mock/wright/releases/$VERSION" && shasum -a 256 "wright-$VERSION-x86_64-unknown-linux-gnu.tar.gz" \ > "wright-$VERSION-x86_64-unknown-linux-gnu.tar.gz.sha256") expect_failure "unknown version fails with an actionable error" "does release" "$WORK/d4" \ @@ -195,7 +200,7 @@ else fi mkdir -p "$WORK/home" -if HOME="$WORK/home" WRIGHT_INSTALL_BASE_URL="$BASE_URL" \ +if HOME="$WORK/home" WRIGHT_INSTALL_BASE_URL="$BASE_URL/wright" \ "$INSTALLER" --version "$VERSION" >/dev/null 2>&1 && test -x "$WORK/home/.local/bin/wright"; then report "default install directory (\$HOME/.local/bin)" ok @@ -204,8 +209,8 @@ else fi # Archive-layout regression: an archive missing wright-lsp must fail cleanly. -make_archive "$WORK/mock-broken" "x86_64-unknown-linux-gnu" -release="$WORK/mock-broken/releases/$VERSION" +make_archive "$WORK/mock-broken/wright" "x86_64-unknown-linux-gnu" +release="$WORK/mock-broken/wright/releases/$VERSION" rm -f "$release/wright-$VERSION-x86_64-unknown-linux-gnu.tar.gz" \ "$release/wright-$VERSION-x86_64-unknown-linux-gnu.tar.gz.sha256" dir="$release/wright-$VERSION-x86_64-unknown-linux-gnu" @@ -221,7 +226,7 @@ BROKEN_PID=$! BROKEN_BASE_URL="http://127.0.0.1:$((PORT + 1))" wait_for_server "$BROKEN_BASE_URL" if WRIGHT_INSTALL_OS=linux WRIGHT_INSTALL_ARCH=x86_64 \ - WRIGHT_INSTALL_BASE_URL="$BROKEN_BASE_URL" \ + WRIGHT_INSTALL_BASE_URL="$BROKEN_BASE_URL/wright" \ "$INSTALLER" --dir "$WORK/d9" --version "$VERSION" >"$INSTALL_OUTPUT" 2>&1; then report "archive missing wright-lsp fails cleanly" fail else