Download, version 0.18.0
An archive per machine, and a checksum the installer will not skip.
A release publishes one relocatable tarball per Unix machine, the windows-x86_64 zip, and one SHA256SUMS file over all of them. Both installers download that file, compare it, and unpack nothing when it does not match. Installing by hand, the same check is one command and one look.
The artifacts
Named the way the installers name them, from the version the changelog states. Each row is what you download, the machine it is for, and the command that prints its digest.
- Linux x86-64uname says Linux x86_64
iyi-0.18.0-linux-x86_64.tar.gzverify
sha256sum iyi-0.18.0-linux-x86_64.tar.gz - macOS arm64uname says Darwin arm64
iyi-0.18.0-darwin-arm64.tar.gzverify
sha256sum iyi-0.18.0-darwin-arm64.tar.gz - Windows x86-64a zip, through install.ps1
iyi-0.18.0-windows-x86_64.zipverify
Get-FileHash -Algorithm SHA256 iyi-0.18.0-windows-x86_64.zip
What that prints has to equal the line for the same file in SHA256SUMS, which is attached to the release. The digests are on the release page, beside the files they verify: a hash copied by hand is exactly the thing a hash exists to catch, and the site cannot read them at build time.
Where sha256sum is missing, install.sh falls back to shasum -a 256, and refuses to continue when neither is there. On Windows there is no fallback to arrange: Get-FileHash is part of the shell.
What the installers check, in their own words
The verification passage from install.sh. The final branch halts with an explicit error if a release published no checksum file.
# The checksum line for this tarball, out of the release's SHA256SUMS.
# Releases before 0.12.0 published none, and that is said rather than
# passed over: an install that was not verified should know it was not.
if curl -fsSL -o "$tmp/SHA256SUMS" "$base/SHA256SUMS" 2>/dev/null; then
expected="$(awk -v name="$asset" '$2 == name || $2 == "*" name { print $1; exit }' "$tmp/SHA256SUMS")"
[ -n "$expected" ] || die "the release's SHA256SUMS has no line for $asset"
if command -v sha256sum >/dev/null 2>&1; then
actual="$(sha256sum "$tmp/$asset" | awk '{ print $1 }')"
elif command -v shasum >/dev/null 2>&1; then
actual="$(shasum -a 256 "$tmp/$asset" | awk '{ print $1 }')"
else
die "the release publishes a checksum and neither sha256sum nor shasum is here to check it"
fi
[ "$actual" = "$expected" ] ||
die "$asset does not match the release's SHA256SUMS: expected $expected, got $actual. Nothing was unpacked; try again, and if it repeats, say so at https://github.com/$repo/issues"
say "verified: sha256 $actual"
else
say "note: release $version publishes no SHA256SUMS; the tarball was not verified"
fiAnd the same passage in the script a Windows reader runs. One SHA256SUMS covers every archive a release publishes, so this is the same file and the same refusal, in the other language. The two names are checked against each other, and against the release job's, when this page is generated.
# The checksum, and no way past it. `install.sh` carries a note for
# releases before 0.12.0, which published none; the Windows zip is
# published from 0.13.0 on and every one of those has a SHA256SUMS, so a
# missing one here is a broken release rather than an old one, and what
# comes out of this file reaches a linker.
$sums = Join-Path $Temp 'SHA256SUMS'
if (-not (Fetch 'SHA256SUMS' $sums -Optional)) {
Die "release $Version publishes no SHA256SUMS, and an unverified zip is not unpacked"
}
$expected = $null
foreach ($line in [IO.File]::ReadAllLines($sums)) {
$fields = $line.Trim() -split '\s+', 2
if ($fields.Count -eq 2 -and $fields[1].TrimStart('*') -eq $Asset) { $expected = $fields[0].ToLower() }
}
if (-not $expected) { Die "the release's SHA256SUMS has no line for $Asset" }
$actual = (Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash.ToLower()
if ($actual -ne $expected) {
Die ("$Asset does not match the release's SHA256SUMS: expected $expected, got $actual. " +
"Nothing was unpacked; try again, and if it repeats, say so at https://github.com/$Repo/issues")
}
Say "verified: sha256 $actual"The file it looks for is the file CI writes
The release job computes the digests over the archives it is about to attach, and attaches them too. Each installer's name for that file and the job's name for it are checked against each other when this page is generated: if they ever differ, the site fails to build rather than telling you an archive was verified.
(cd dist && sha256sum *.tar.gz *.zip > SHA256SUMS && cat SHA256SUMS)
gh release create "$tag" --title "iyi ${tag#v}" --notes-file notes.md \And both refusals are tested
Before any of that ships, CI corrupts one digit of the published checksum file, runs the installer against it, and requires three things: that it fails, that it fails with its own sentence, and that no binary is left behind. The Windows job does it to the zip.
sed -i '1s/^0/X/; 1s/^[1-9a-f]/0/' dist/SHA256SUMS
if IYI_PREFIX="$PWD/tampered" sh install.sh 2>err; then
echo "a tarball that does not match SHA256SUMS was installed"; exit 1
fi
grep -q "does not match the release's SHA256SUMS" err
[ ! -e tampered/bin/iyi ]# One hex digit wrong. `-RedirectStandardError`, because the
# refusal is written to the process's own stderr and a `2>` inside
# PowerShell only redirects PowerShell's error stream, which is how
# this check first passed while capturing nothing.
$sums = Join-Path $dist 'SHA256SUMS'
$text = [IO.File]::ReadAllText($sums)
$first = if ($text[0] -eq '0') { 'f' } else { '0' }
[IO.File]::WriteAllText($sums, $first + $text.Substring(1))
$tampered = Join-Path $env:RUNNER_TEMP 'tampered'
$refusal = Join-Path $env:RUNNER_TEMP 'refusal.txt'
$child = Start-Process -FilePath 'pwsh' -RedirectStandardError $refusal -NoNewWindow -Wait -PassThru `
-ArgumentList @('-NoProfile', '-File', '.\install.ps1', '-ReleaseUrl', "`"$dist`"",
'-Version', $version, '-Prefix', "`"$tampered`"", '-NoPath')
$message = [IO.File]::ReadAllText($refusal)
Write-Host $message
if ($child.ExitCode -eq 0) { throw 'a zip that does not match SHA256SUMS was installed' }
if ($message -notmatch "does not match the release's SHA256SUMS") {
throw 'the refusal did not say why'
}
if (Test-Path (Join-Path $tampered 'bin\iyi.exe')) { throw 'something was unpacked anyway' }Machines it refuses
install.sh ships linux-x86_64 and darwin-arm64 and install.ps1 ships windows-x86_64. Anything else gets this refusal and a pointer to building from source:
no release for $os $arch: releases ship linux-x86_64 and darwin-arm64, see README.md (Getting it) to build from source
case "$os-$arch" in
Linux-x86_64) target=linux-x86_64 ;;
Darwin-arm64) target=darwin-arm64 ;;
*) die "no release for $os $arch: releases ship linux-x86_64 and darwin-arm64, see README.md (Getting it) to build from source" ;;
esacOther platforms build from source. The compiler cross-compiles for 9 targets; these are the ones a release job produces an archive for.
The knobs
The script documents three environment variables and reads exactly those:
IYI_PREFIXwhere to unpack, default ~/.local; the tarball is relocatable
defaults to
$HOME/.localIYI_VERSIONa release to pin, e.g. 0.18.0; default is the latest release
unset by default
IYI_RELEASE_URLwhere the release's files are, default the GitHub release; CI points it at a directory (file://) to install a tarball before it is released, which is how this script is gated
defaults to
https://github.com/$repo/releases/download/v$version
# IYI_PREFIX where to unpack, default ~/.local; the tarball is relocatable
# IYI_VERSION a release to pin, e.g. 0.18.0; default is the latest release
# IYI_RELEASE_URL where the release's files are, default the GitHub release;
# CI points it at a directory (file://) to install a tarball
# before it is released, which is how this script is gatedEvery release
The changelog lives in the repository, where it stays current with the code. Notes for each release are on its own page there, written from the tag's own message.
0.18.02026-10-180.17.02026-10-140.16.22026-10-020.16.12026-10-010.16.02026-09-300.15.42026-09-280.15.32026-09-270.15.22026-09-260.15.12026-09-250.15.02026-09-250.14.12026-09-240.14.02026-09-220.13.02026-09-160.12.02026-09-110.11.02026-09-070.10.02026-09-050.9.02026-09-030.8.02026-09-010.7.02026-09-010.6.02026-09-010.5.12026-08-300.5.02026-08-290.4.02026-08-280.3.02026-08-260.2.02026-08-200.1.02026-08-18