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