ci / lint-compose (push) Successful in 14s
ci / lint-actionlint (push) Successful in 10s
ci / lint-shellcheck (push) Successful in 11s
ci / lint-prettier (push) Successful in 17s
ci / lint-ruff (push) Successful in 9s
ci / lint-yaml (push) Successful in 12s
ci / lint-dockerfiles (push) Successful in 9s
ci / validate (push) Successful in 9s
renovate-ci / validate-renovate (push) Successful in 1m34s
ci / build (push) Failing after 3m3s
Callers prepend BIN_DIR to PATH only after the script exits, so the bare uv invocation in install_uv_tool died with 127 on clean runners. Export BIN_DIR to PATH inside the script and invoke the just-installed binary by absolute path.
255 lines
8.1 KiB
Bash
Executable File
255 lines
8.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Installs the pinned CI tools 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"
|
|
# The just-installed tools must resolve inside this script too: callers only
|
|
# prepend BIN_DIR to PATH after the script exits, so a bare `uv` below would
|
|
# miss the binary install_uv just placed (exit 127 on a clean runner).
|
|
export PATH="$BIN_DIR:$PATH"
|
|
|
|
arch="$(uname -m)"
|
|
# Upstream projects disagree on arch spelling: kubeconform and actionlint use
|
|
# Go names (amd64/arm64), shellcheck uses uname names (x86_64/aarch64), node
|
|
# uses neither (x64/arm64), and hadolint mixes the two in a single release
|
|
# (x86_64 but arm64).
|
|
case "$arch" in
|
|
x86_64 | amd64)
|
|
goarch=amd64
|
|
sharch=x86_64
|
|
nodearch=x64
|
|
hadolintarch=x86_64
|
|
;;
|
|
aarch64 | arm64)
|
|
goarch=arm64
|
|
sharch=aarch64
|
|
nodearch=arm64
|
|
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
|
|
}
|
|
|
|
# resolve <command>
|
|
# Absolute path to use for invoking a tool: the copy in BIN_DIR when present,
|
|
# otherwise the name for PATH lookup. Every version check and every in-script
|
|
# invocation goes through this, so a tool missing from both places reads as
|
|
# "not installed" instead of dying with 127 under `set -e`.
|
|
resolve() {
|
|
if [ -x "$BIN_DIR/$1" ]; then
|
|
printf '%s' "$BIN_DIR/$1"
|
|
else
|
|
printf '%s' "$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 bin out
|
|
bin="$(resolve "$1")"
|
|
if ! command -v "$bin" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
case "$1" in
|
|
kubeconform) out="$("$bin" -v 2>/dev/null | head -1 || true)" ;;
|
|
*) out="$("$bin" --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" "$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_pip_audit() {
|
|
install_uv_tool pip-audit "${PIP_AUDIT_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_node() {
|
|
# npm gets checked by running it, not by looking it up: what matters is that
|
|
# it answers, so a stub, a half-removed Arch package or a name that resolves
|
|
# to something broken all have to read as "not installed". The runner's npm
|
|
# is a symlink into /usr/lib/node_modules/npm, which is exactly the kind of
|
|
# thing that disappears between runs.
|
|
if at_version node "v${NODE_VERSION}" && [ -n "$(installed_version npm)" ]; then
|
|
return 0
|
|
fi
|
|
# Same shape as prettier above: the tarball's bin/npm and bin/npx are links
|
|
# into lib/node_modules, so the whole tree has to survive next to them.
|
|
local dir="$BIN_DIR/node-${NODE_VERSION}"
|
|
if [ ! -x "$dir/bin/node" ]; then
|
|
rm -rf "$dir"
|
|
mkdir -p "$dir"
|
|
fetch "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${nodearch}.tar.xz" \
|
|
"$dir/node.tar.xz"
|
|
tar -xJf "$dir/node.tar.xz" -C "$dir" --strip-components=1 "node-v${NODE_VERSION}-linux-${nodearch}"
|
|
rm -f "$dir/node.tar.xz"
|
|
fi
|
|
# Relative, so the whole tree stays valid if TOOLS_DIR is relocated.
|
|
for bin in node npm npx; do
|
|
ln -sfn "node-${NODE_VERSION}/bin/${bin}" "$BIN_DIR/${bin}"
|
|
done
|
|
}
|
|
|
|
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 ;;
|
|
pip-audit) install_pip_audit ;;
|
|
hadolint) install_hadolint ;;
|
|
node) install_node ;;
|
|
uv) install_uv ;;
|
|
*)
|
|
echo "install-ci-tools: unknown tool: $tool" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
printf '%s\n' "$BIN_DIR"
|