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
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,19 @@ jobs:
fi
echo "release notes are for v$version"

# The installer verifies the file it downloaded, not whatever is listed
# first. The script read the first `sha256:` in the release JSON, which
# was correct while a release carried one asset and became wrong the
# moment the Linux tarballs were added: GitHub lists assets in upload
# order, so the first digest belongs to whichever packaging job finished
# first. On v0.10.11 that is the Linux aarch64 tarball, so every macOS
# install failed with a checksum mismatch on a perfectly good download.
#
# The checks live in the script so they can be run by hand, and so this
# file does not carry shell that only ever runs on a runner.
- name: The macOS installer verifies the macOS download
run: scripts/check-macos-installer.sh ${{ github.repository }} .

# The GUI (Native SDK): macOS only, via the native CLI.
gui:
runs-on: macos-latest
Expand Down
70 changes: 70 additions & 0 deletions scripts/check-macos-installer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# The installer verifies the file it downloaded, not whatever is listed first.
#
# Two checks, because they catch different things.
set -eu
repo="$1"
prefix="$2"

extract() { printf '%s' "$1" | tr -d '\n' | tr '{' '\n' | grep 'macos\.zip' | grep -o 'sha256:[0-9a-f]\{64\}' | head -1 | cut -d: -f2 || true; }

# 1. A FIXTURE with a Linux asset first. This is the case that broke, and it
# breaks deterministically whether or not the live release happens to be
# ordered that way today. Pretty-printed and carrying an `uploader` object,
# because the real response is both and a flat fixture would pass against
# code that cannot read the real thing.
fixture='{
"tag_name": "v9.9.9",
"assets": [
{
"name": "notary-9.9.9-linux-aarch64.tar.gz",
"uploader": { "login": "github-actions[bot]", "id": 41898282 },
"digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
"browser_download_url": "https://example.com/notary-9.9.9-linux-aarch64.tar.gz"
},
{
"name": "Notary-v9.9.9-macos.zip",
"uploader": { "login": "github-actions[bot]", "id": 41898282 },
"digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
"browser_download_url": "https://example.com/Notary-v9.9.9-macos.zip"
}
]
}'
got="$(extract "$fixture")"
if [ "$got" != "2222222222222222222222222222222222222222222222222222222222222222" ]; then
echo "With a Linux asset listed first, the installer picks the wrong digest."
echo " picked: ${got:-nothing}"
echo " wanted: 2222... (the macOS zip)"
echo "That is the same failure Plaza hit at v0.19.0: a correct download reported as corrupt."
exit 1
fi
echo "ok: a Linux asset listed first does not steal the macOS digest"

# 2. The LIVE release, because a fixture only encodes what I think the response
# looks like, and what can break this again is GitHub changing exactly that.
json="$(curl -fsSL "https://api.github.com/repos/$repo/releases/latest")"
want="$(printf '%s' "$json" | python3 -c 'import json,sys
d=json.load(sys.stdin)
for a in d.get("assets",[]):
if a["name"].endswith("macos.zip"):
print((a.get("digest") or "").replace("sha256:",""))
break')"
if [ -z "$want" ]; then
echo "ok: the latest release publishes no macOS digest, nothing to compare"
else
got="$(extract "$json")"
if [ "$got" != "$want" ]; then
echo "Against the live release, the installer picks the wrong digest."
echo " picked: ${got:-nothing}"
echo " wanted: $want"
exit 1
fi
echo "ok: against the live release the installer picks the macOS digest ($want)"
fi

# 3. And the script that ships is the one these checks describe.
grep -q "tr -d '\\\\n' | tr '{' " "$prefix/scripts/install-macos.sh" || {
echo "$prefix/scripts/install-macos.sh no longer flattens before splitting."
exit 1
}
bash -n "$prefix/scripts/install-macos.sh"
echo "ok: the shipped script is the one that was checked"
26 changes: 25 additions & 1 deletion scripts/install-macos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,31 @@ main() {
local tag url digest
tag="$(printf '%s' "$json" | grep -o '"tag_name":[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*"([^"]+)".*/\1/')"
url="$(printf '%s' "$json" | grep -o '"browser_download_url":[[:space:]]*"[^"]*macos\.zip"' | head -1 | sed -E 's/.*"(https[^"]+)".*/\1/')"
digest="$(printf '%s' "$json" | grep -o 'sha256:[0-9a-f]\{64\}' | head -1 | cut -d: -f2)"
# The digest OF THE FILE BEING DOWNLOADED, which is not the same thing as the
# first digest in the release.
#
# This used to be `grep -o 'sha256:...' | head -1` over the whole response,
# which takes the first digest in the JSON whatever asset it belongs to.
# Correct while a release carried one asset, and wrong from the moment the
# Linux tarballs were added: GitHub lists assets in upload order, so the first
# digest belongs to whichever packaging job finished first. On v0.10.11 that
# is notary-0.10.11-linux-aarch64.tar.gz, so every macOS install fails with a
# checksum mismatch on a download that is perfectly fine.
#
# That is the worst way for a checksum to fail. The bytes are right and the
# comparison is against something else, so the tool tells people their
# download is corrupt and refuses. A check that cries wolf teaches people to
# skip checks.
#
# So the digest is read from the asset that names the file. The response is
# pretty-printed, so it is FLATTENED FIRST and only then split on the `{` that
# starts each object: without the flatten every field is already on its own
# line and the name and the digest can never meet. The segment naming the
# macOS zip ends at the next asset's `{`, so it cannot reach a neighbour's.
#
# No jq. This runs before anything is installed and uses only what macOS
# already ships. Plaza carries the identical fix and the identical guard.
digest="$(printf '%s' "$json" | tr -d '\n' | tr '{' '\n' | grep 'macos\.zip' | grep -o 'sha256:[0-9a-f]\{64\}' | head -1 | cut -d: -f2 || true)"

[ -n "$url" ] || die "no macOS build found on the latest release (${tag:-unknown})."
say "Latest release: ${tag:-unknown}"
Expand Down
Loading