prettier, ruff, yamllint and hadolint were the only CI tools still called bare, straight off whatever the runner happened to have installed. Pin them in tool-versions.env like the other three and install them the same way, so the versions Renovate moves are the versions CI runs. Each pinned version equals what is already on the runner, so this changes what CI does not at all today. It changes what CI does on a rebuilt runner: the pinned one gets installed over the drift. The four need four different mechanisms, which is why this is not one pattern: hadolint a bare binary per platform, like actionlint ruff, yamllint PyPI wheels, unpacked by uv prettier an npm tarball, unpacked by tar prettier is the awkward one. Its entry point requires ../package.json relative to its own real path, so copying the single file out -- which is what every other installer here does -- yields a module-not-found at the first run. It keeps its package directory in a versioned one next to a relative symlink, and the tarball ships bin/ without the exec bit, so that needs chmod too. hadolint's release names one platform uname-style and the other Go-style (x86_64 but arm64), which 404s on the first architecture if you assume otherwise. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
199 lines
5.9 KiB
Bash
Executable File
199 lines
5.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Installs the pinned CI linters into "$TOOLS_DIR/bin" and echoes that directory
|
|
# on stdout, so callers can do:
|
|
#
|
|
# export PATH="$(bash .gitea/workflows/install-ci-tools.sh kubeconform shellcheck):$PATH"
|
|
#
|
|
# Versions come from tool-versions.env next to this script and are kept fresh by
|
|
# Renovate. Re-running is cheap: an already-installed tool at the pinned version
|
|
# is left alone.
|
|
set -euo pipefail
|
|
|
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=tool-versions.env
|
|
. "$here/tool-versions.env"
|
|
|
|
TOOLS_DIR="${TOOLS_DIR:-${RUNNER_TEMP:-/tmp}/homelab-tools}"
|
|
BIN_DIR="$TOOLS_DIR/bin"
|
|
mkdir -p "$BIN_DIR"
|
|
|
|
arch="$(uname -m)"
|
|
# Upstream projects disagree on arch spelling: kubeconform and actionlint use
|
|
# Go names (amd64/arm64), shellcheck uses uname names (x86_64/aarch64), and
|
|
# hadolint mixes the two in a single release (x86_64 but arm64).
|
|
case "$arch" in
|
|
x86_64 | amd64)
|
|
goarch=amd64
|
|
sharch=x86_64
|
|
hadolintarch=x86_64
|
|
;;
|
|
aarch64 | arm64)
|
|
goarch=arm64
|
|
sharch=aarch64
|
|
hadolintarch=arm64
|
|
;;
|
|
*)
|
|
echo "install-ci-tools: unsupported architecture: $arch" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
fetch() {
|
|
# fetch <url> <dest>
|
|
if command -v curl >/dev/null 2>&1; then
|
|
curl -sSLf --retry 3 -o "$2" "$1"
|
|
elif command -v wget >/dev/null 2>&1; then
|
|
wget -q -O "$2" "$1"
|
|
else
|
|
echo "install-ci-tools: neither curl nor wget is available" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# installed_version <command>
|
|
# Prints the version of an already-installed tool, or nothing. Each tool spells
|
|
# its version flag differently, hence the case.
|
|
installed_version() {
|
|
local out
|
|
case "$1" in
|
|
kubeconform) out="$("$1" -v 2>/dev/null | head -1 || true)" ;;
|
|
*) out="$("$1" --version 2>/dev/null | head -1 || true)" ;;
|
|
esac
|
|
printf '%s' "$out"
|
|
}
|
|
|
|
# at_version <command> <expected>
|
|
at_version() {
|
|
case "$(installed_version "$1")" in
|
|
*"$2"*) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
install_kubeconform() {
|
|
if at_version kubeconform "v${KUBECONFORM_VERSION}"; then
|
|
return 0
|
|
fi
|
|
local tmp
|
|
tmp="$(mktemp -d)"
|
|
fetch "https://github.com/yannh/kubeconform/releases/download/v${KUBECONFORM_VERSION}/kubeconform-linux-${goarch}.tar.gz" \
|
|
"$tmp/kubeconform.tar.gz"
|
|
tar -xzf "$tmp/kubeconform.tar.gz" -C "$tmp" kubeconform
|
|
install -m 0755 "$tmp/kubeconform" "$BIN_DIR/kubeconform"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
install_shellcheck() {
|
|
if at_version shellcheck "${SHELLCHECK_VERSION}"; then
|
|
return 0
|
|
fi
|
|
local tmp
|
|
tmp="$(mktemp -d)"
|
|
fetch "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.linux.${sharch}.tar.xz" \
|
|
"$tmp/shellcheck.tar.xz"
|
|
tar -xJf "$tmp/shellcheck.tar.xz" -C "$tmp" --strip-components=1 "shellcheck-v${SHELLCHECK_VERSION}/shellcheck"
|
|
install -m 0755 "$tmp/shellcheck" "$BIN_DIR/shellcheck"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
install_uv() {
|
|
if at_version uv "${UV_VERSION}"; then
|
|
return 0
|
|
fi
|
|
local tmp
|
|
tmp="$(mktemp -d)"
|
|
# uv release tags carry no leading v, unlike every other tool installed here.
|
|
fetch "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${sharch}-unknown-linux-gnu.tar.gz" \
|
|
"$tmp/uv.tar.gz"
|
|
tar -xzf "$tmp/uv.tar.gz" -C "$tmp" --strip-components=1 "uv-${sharch}-unknown-linux-gnu/uv"
|
|
install -m 0755 "$tmp/uv" "$BIN_DIR/uv"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
install_hadolint() {
|
|
if at_version hadolint "${HADOLINT_VERSION}"; then
|
|
return 0
|
|
fi
|
|
# A bare binary, no archive: hadolint ships one file per platform.
|
|
fetch "https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-linux-${hadolintarch}" \
|
|
"$BIN_DIR/hadolint"
|
|
chmod 0755 "$BIN_DIR/hadolint"
|
|
}
|
|
|
|
# ruff and yamllint both come from PyPI as wheels, which uv unpacks for us.
|
|
install_uv_tool() {
|
|
# <package> <pinned version>
|
|
if at_version "$1" "$2"; then
|
|
return 0
|
|
fi
|
|
install_uv
|
|
UV_TOOL_BIN_DIR="$BIN_DIR" uv tool install --force "$1==$2" >/dev/null
|
|
}
|
|
|
|
install_ruff() {
|
|
install_uv_tool ruff "${RUFF_VERSION}"
|
|
}
|
|
|
|
install_yamllint() {
|
|
install_uv_tool yamllint "${YAMLLINT_VERSION}"
|
|
}
|
|
|
|
install_prettier() {
|
|
if at_version prettier "${PRETTIER_VERSION}"; then
|
|
return 0
|
|
fi
|
|
# Not a standalone binary: prettier's entry point requires ../package.json
|
|
# relative to its own real path, so the package directory has to survive
|
|
# next to it. Hence a versioned directory plus a relative symlink, rather
|
|
# than copying the one file out as the other installers do.
|
|
local dir="$BIN_DIR/prettier-${PRETTIER_VERSION}"
|
|
if [ ! -f "$dir/package/package.json" ]; then
|
|
rm -rf "$dir"
|
|
mkdir -p "$dir"
|
|
fetch "https://registry.npmjs.org/prettier/-/prettier-${PRETTIER_VERSION}.tgz" "$dir/prettier.tgz"
|
|
tar -xzf "$dir/prettier.tgz" -C "$dir"
|
|
rm -f "$dir/prettier.tgz"
|
|
# npm strips the exec bit from bin/ on the way into the tarball.
|
|
chmod 0755 "$dir/package/bin/prettier.cjs"
|
|
fi
|
|
# Relative, so the whole tree stays valid if TOOLS_DIR is relocated.
|
|
ln -sfn "prettier-${PRETTIER_VERSION}/package/bin/prettier.cjs" "$BIN_DIR/prettier"
|
|
}
|
|
|
|
install_actionlint() {
|
|
if at_version actionlint "${ACTIONLINT_VERSION}"; then
|
|
return 0
|
|
fi
|
|
local tmp
|
|
tmp="$(mktemp -d)"
|
|
fetch "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_${goarch}.tar.gz" \
|
|
"$tmp/actionlint.tar.gz"
|
|
tar -xzf "$tmp/actionlint.tar.gz" -C "$tmp" actionlint
|
|
install -m 0755 "$tmp/actionlint" "$BIN_DIR/actionlint"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
wanted=("$@")
|
|
if [ "${#wanted[@]}" -eq 0 ]; then
|
|
wanted=(kubeconform shellcheck actionlint prettier ruff yamllint hadolint)
|
|
fi
|
|
|
|
for tool in "${wanted[@]}"; do
|
|
case "$tool" in
|
|
kubeconform) install_kubeconform ;;
|
|
shellcheck) install_shellcheck ;;
|
|
actionlint) install_actionlint ;;
|
|
prettier) install_prettier ;;
|
|
ruff) install_ruff ;;
|
|
yamllint) install_yamllint ;;
|
|
hadolint) install_hadolint ;;
|
|
uv) install_uv ;;
|
|
*)
|
|
echo "install-ci-tools: unknown tool: $tool" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
printf '%s\n' "$BIN_DIR"
|