iyi

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.

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.

install.sh, download and verifyinstall.sh, lines 59 to 77
# 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"
fi

And 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.

install.ps1, download and verifyinstall.ps1, lines 170 to 192
# 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.

the release job.github/workflows/iyi.yml, lines 3034 to 3035
(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.

the gate over install.sh.github/workflows/iyi.yml, lines 2960 to 2965
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 ]
the gate over install.ps1.github/workflows/iyi.yml, lines 2209 to 2228
# 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

install.sh, the uname caseinstall.sh, lines 33 to 37
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" ;;
esac

Other 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_PREFIX

where to unpack, default ~/.local; the tarball is relocatable

defaults to $HOME/.local

IYI_VERSION

a release to pin, e.g. 0.18.0; default is the latest release

unset by default

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 gated

defaults to https://github.com/$repo/releases/download/v$version

install.sh, the headerinstall.sh, lines 6 to 10
# 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 gated

Every 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.