From 61f0b2bdd719850e763a20c987b2fadbace7cb06 Mon Sep 17 00:00:00 2001 From: Zack Maril Date: Mon, 24 Aug 2026 18:31:31 -0400 Subject: [PATCH] Say why the install failed `install.sh` piped the release fetch straight into `sed`, so the fetch's exit status was hidden behind `head`. A refused request parsed to an empty string and took the "no published release found" path, which reads as "this repository has no releases" and sends whoever hit it looking in the wrong place. It cost three attempts to diagnose in a downstream repository today, and the cause was rate limiting. The response is now read before it is parsed, so the accurate message fires; that message says what is usually true, which is that GitHub allows 60 unauthenticated API requests an hour per address and CI runners share addresses. Downloads retry three times, because a reset connection on the way to a release is not a reason to fail a build -- the same job failed that way once before it failed with the 403. The action annotates an installation failure as one. In a checks list "could not install" and "found something" are both a red job ending in `exit code 1`, and the difference matters to whoever opens it. The `token` input said it was needed only for a private repository, which is true of access and false of rate limits; it now says both. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01361quprG7tUgaVFKAmKtJZ --- CHANGELOG.md | 16 ++++++++++++++++ action.yml | 16 +++++++++++++--- install.sh | 32 +++++++++++++++++++++++++------- 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cbbcd1..ee44feb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed + +- `install.sh` reported a refused request as a repository with no releases. The + fetch was piped straight into `sed`, so its exit status was hidden behind + `head` and an empty parse looked like an empty repository -- the message sent + whoever read it looking in the wrong place. It now reads the response before + parsing it, and says what is usually true: GitHub allows 60 unauthenticated + API requests an hour per address, CI runners share addresses, and the fix is + a token or a pinned version. +- Downloads retry. A reset connection on the way to a release is not a reason + to fail a build. +- A failed installation says so. In a checks list an install that could not + happen and a scan that found something are both a red job ending in `exit + code 1`; the action now annotates the first as an installation failure, and + points at the `token` input when rate limiting is the cause. + ### Added - `site/` — the source for [straitjacket.dev](https://straitjacket.dev), moved diff --git a/action.yml b/action.yml index 6bf61d5..ce4ad28 100644 --- a/action.yml +++ b/action.yml @@ -75,8 +75,11 @@ inputs: default: "true" token: description: >- - Token used to download the release. Needed only while the Straitjacket - repository is private; a public release needs none. + Token used to read the release. Worth setting even for a public release: + GitHub allows 60 unauthenticated API requests an hour per address and CI + runners share addresses, so without one this eventually fails to install + at all. `${{ github.token }}` is enough; it needs no access to the + Straitjacket repository while that repository is public. required: false default: "" @@ -89,6 +92,7 @@ runs: using: composite steps: - name: Install straitjacket + id: install shell: bash env: VERSION: ${{ inputs.version }} @@ -100,7 +104,13 @@ runs: if [ "$VERSION" != "latest" ]; then export STRAITJACKET_VERSION="$VERSION" fi - sh "${ACTION_PATH}/install.sh" + # Failing to install is not the same as finding something, and at a + # glance in the checks list the two look identical: both are a red job + # whose log ends in `exit code 1`. Say which one this is. + if ! sh "${ACTION_PATH}/install.sh"; then + echo "::error title=Straitjacket could not be installed::The scan did not run. This is an installation failure, not a finding -- see the log above, and set the action's \`token\` input if it mentions rate limiting." >&2 + exit 1 + fi echo "$STRAITJACKET_INSTALL_DIR" >> "$GITHUB_PATH" # Inputs reach the script through the environment rather than through diff --git a/install.sh b/install.sh index 1ee2631..dd5daa6 100755 --- a/install.sh +++ b/install.sh @@ -50,12 +50,18 @@ detect_target() { esac } +# Retry a few times, because a reset connection on the way to a release is +# not a reason to fail a build. Rate limiting is not retried away -- see the +# message in `main` -- but a dropped connection usually is. +RETRY="--retry 3 --retry-delay 2" + # Fetch a URL to stdout, sending credentials only when they were provided. fetch() { + # shellcheck disable=SC2086 if [ -n "${GITHUB_TOKEN:-}" ]; then - curl -sSfL -H "Authorization: Bearer ${GITHUB_TOKEN}" "$1" + curl -sSfL $RETRY -H "Authorization: Bearer ${GITHUB_TOKEN}" "$1" else - curl -sSfL "$1" + curl -sSfL $RETRY "$1" fi } @@ -63,8 +69,14 @@ fetch() { # # Parsed out of the API response with sed rather than a JSON library, because # the whole point of this script is to run before anything is installed. +# +# The response is captured before it is parsed. Piping the fetch straight into +# sed hides its exit status behind `head`, so a refused request looked exactly +# like a repository with no releases -- and the message said so, which sent +# whoever read it looking in the wrong place. latest_version() { - fetch "${API}/releases/latest" | + response=$(fetch "${API}/releases/latest") || return 1 + printf '%s\n' "$response" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1 } @@ -135,10 +147,12 @@ download_asset() { elif [ -n "${GITHUB_TOKEN:-}" ]; then asset_api=$(fetch "${API}/releases/tags/${version}" | asset_url "$asset_name") [ -n "$asset_api" ] || die "release ${version} has no asset named ${asset_name}" - curl -sSfL -H "Authorization: Bearer ${GITHUB_TOKEN}" \ + # shellcheck disable=SC2086 + curl -sSfL $RETRY -H "Authorization: Bearer ${GITHUB_TOKEN}" \ -H "Accept: application/octet-stream" "$asset_api" >"$asset_output" else - curl -sSfL "${DOWNLOAD}/${version}/${asset_name}" >"$asset_output" + # shellcheck disable=SC2086 + curl -sSfL $RETRY "${DOWNLOAD}/${version}/${asset_name}" >"$asset_output" fi } @@ -150,8 +164,12 @@ main() { target=$(detect_target) version=${STRAITJACKET_VERSION:-} if [ -z "$version" ]; then - version=$(latest_version) || - die "could not read the latest release. Set STRAITJACKET_VERSION, or GITHUB_TOKEN if the repository is private" + version=$(latest_version) || die "could not read the latest release. + GitHub allows 60 unauthenticated API requests an hour per address, and CI + runners share addresses, so this is usually rate limiting rather than a + missing release. Set GITHUB_TOKEN -- any valid token raises the limit, and + it need not have access to this repository while it is public. Or pin + STRAITJACKET_VERSION, which skips the API entirely." [ -n "$version" ] || die "no published release found. Set STRAITJACKET_VERSION to install a specific tag" fi