The runner executes jobs on the host rather than in a container, so a
workflow that says 'python3' or 'npm' is really saying 'whatever this
machine happens to have today'. Both of the test jobs added in c00a472
were red on the first run for exactly that reason.
uv venv with no --python takes the first interpreter it finds, which is
the host's 3.14 here. pyrogram's sync.py calls the bare
asyncio.get_event_loop() that 3.14 no longer auto-creates, so three
tests died at collection. Pinned to 3.13, which is both what uv will
fetch when the host has none and what python:3.13-slim actually builds.
npm was missing outright, and turned up an hour later as npm 12 on node
26 - the same push, minutes apart. Neither is the panel image's
node:22-alpine, so node is now installed from the official tarball the
way the other tools are, at the image's major. npm is checked by running
it rather than by looking it up, so a name that resolves to something
broken reads as not installed.
Renovate keeps NODE_VERSION in step with the Dockerfile's node: tag, and
the two are one grouped dependency: CI that tests on a different major
than it builds on is a gate that can pass over a real break.
234 lines
7.3 KiB
Bash
Executable File
234 lines
7.3 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"
|
|
|
|
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
|
|
}
|
|
|
|
# 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_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"
|