Skip to content
Open
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 }}
47 changes: 47 additions & 0 deletions install
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,51 @@ 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"
rm -rf "$tmp_dir"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Checksum mismatch path references undefined tmp_dir under set -u. This triggers an unbound-variable error and breaks intended error-handling cleanup logic.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At install, line 398:

<comment>Checksum mismatch path references undefined `tmp_dir` under `set -u`. This triggers an unbound-variable error and breaks intended error-handling cleanup logic.</comment>

<file context>
@@ -356,6 +356,51 @@ download_with_progress() {
+        print_message error "Checksum mismatch for $name"
+        print_message error "  expected: $expected"
+        print_message error "  actual:   $actual"
+        rm -rf "$tmp_dir"
+        exit 1
+    fi
</file context>
Suggested change
rm -rf "$tmp_dir"
rm -rf "$(dirname "$file")"

exit 1
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 +412,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
46 changes: 38 additions & 8 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,36 @@ 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 {
$sums = (Invoke-WebRequest -Uri $ChecksumsUrl -UseBasicParsing).Content

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — verification is effectively dead on Windows PowerShell 5.1.

GitHub release-assets are served with Content-Type: application/octet-stream (verified with curl -I against an existing release-asset checksums file). On Windows PowerShell 5.1, Invoke-WebRequest -UseBasicParsing returns .Content as System.Byte[] (not String) whenever the content-type isn't text-recognized. PS 5.1 is the default shell on Windows 10 and is preinstalled alongside PS 7 on Windows 11.

Downstream effect:

  • $sums = (... ).Content → byte array, not text
  • $sums -split "n"→ PowerShell coerces the byte[] to a"49 50 51 …"decimal string before splitting → no\n` boundaries → one element
  • Where-Object { $_ -match … } → no match
  • Falls into the "no checksum entry for X" soft-skip branch every time

So on the dominant Windows shell, every install soft-skips verification and the user sees a misleading "no checksum entry" notice instead of either a real check or a clear "unsupported" error.

Fix — decode bytes explicitly:

$resp = Invoke-WebRequest -Uri $ChecksumsUrl -UseBasicParsing
$sums = if ($resp.Content -is [byte[]]) {
    [System.Text.Encoding]::UTF8.GetString($resp.Content)
} else {
    $resp.Content
}

Worth a Pester test that feeds a known-good fixture through Test-Checksum so this regression can't come back.

} 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 Down Expand Up @@ -171,10 +201,12 @@ function Install-Target {
$filename = "$App-$target.zip"

if ($useLatest) {
$url = "https://github.com/AltimateAI/altimate-code/releases/latest/download/$filename"
$base = "https://github.com/AltimateAI/altimate-code/releases/latest/download"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Using the mutable releases/latest/download URL for checksum verification creates a race: archive and checksums can come from different releases. This can cause transient hard-fail installs even when artifacts are valid.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At install.ps1, line 204:

<comment>Using the mutable `releases/latest/download` URL for checksum verification creates a race: archive and checksums can come from different releases. This can cause transient hard-fail installs even when artifacts are valid.</comment>

<file context>
@@ -171,10 +201,12 @@ function Install-Target {
 
   if ($useLatest) {
-    $url = "https://github.com/AltimateAI/altimate-code/releases/latest/download/$filename"
+    $base = "https://github.com/AltimateAI/altimate-code/releases/latest/download"
   } else {
-    $url = "https://github.com/AltimateAI/altimate-code/releases/download/v$specificVersion/$filename"
</file context>
Suggested change
$base = "https://github.com/AltimateAI/altimate-code/releases/latest/download"
$base = "https://github.com/AltimateAI/altimate-code/releases/download/v$specificVersion"

} 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 +216,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 +227,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 Down
49 changes: 49 additions & 0 deletions packages/opencode/test/install/checksum-verification.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* 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"))
})
})
Loading