#!/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 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 # 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 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() { # 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"