Two things, both about not finding out late. No workflow declared `permissions`, so all eighteen jobs across the four workflows ran on a token with the default full repository scope. Every one of them only checks out code, and deploy reaches the cluster over SSH with the deploy key, and Renovate writes through its own bot PAT rather than the Actions token. So `contents: read` is all any of them needed. The panel image ships 15 known advisories and nothing was looking. Add a scan-deps job that fails on anything new, and record the eight current ones by ID in the workflow. It is a list rather than a baseline count so that the diff that accepts an advisory says so in words, and it lives in our workflow instead of the package manifest so a subtree sync from forust/userbot cannot quietly widen the exemption. Both halves were checked to fail on a regression, not just to pass today: removing one --ignore-vuln turns the Python step red, and dropping --audit-level to moderate turns the npm one red on the devalue advisory. npm audits production dependencies only. All seven findings in the full tree are build- or test-time: the esbuild advisory needs a vite dev server exposed to the internet, and nanoid's infinite loop needs a custom generator called with size 0, which postcss does not do. None are in the 91 kB bundle the panel serves, so failing on them would be noise that trains people to ignore the job. The starlette entries are the reason the job is not "fail on everything": fastapi 0.115.12 pins starlette<0.47.0 and the last four fixes need 0.49.1 through 1.3.1, so clearing them is a jump to fastapi 0.141.x and is upstream's call, not a drive-by. Four of the seven are reachable in principle, which the comment on the job sets out. The panel answers only on userbot.workstation.internal with no public route, which is what keeps those four from being an internet-facing DoS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
204 lines
6.1 KiB
Bash
Executable File
204 lines
6.1 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_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_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 ;;
|
|
uv) install_uv ;;
|
|
*)
|
|
echo "install-ci-tools: unknown tool: $tool" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
printf '%s\n' "$BIN_DIR"
|