From 30995ee00930384506d913e2c48be4a393e445fe Mon Sep 17 00:00:00 2001 From: mr-forust Date: Sun, 27 Sep 2026 10:28:42 +0200 Subject: [PATCH] ci: pin the last four linters instead of trusting the runner 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) --- .gitea/workflows/ci.yaml | 21 +++++++--- .gitea/workflows/install-ci-tools.sh | 61 +++++++++++++++++++++++++++- .gitea/workflows/tool-versions.env | 13 +++++- renovate/k8s/configmap.yaml | 32 +++++++++++++++ renovate/renovate.json | 32 +++++++++++++++ 5 files changed, 151 insertions(+), 8 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index f79b656..5ef1e06 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -106,6 +106,10 @@ jobs: - name: Check formatting with Prettier shell: bash run: | + set -euo pipefail + tools_dir="$(bash .gitea/workflows/install-ci-tools.sh prettier)" + export PATH="$tools_dir:$PATH" + mapfile -t prettier_files < <( git ls-files \ | grep -E '\.(md|json|ya?ml|html|css)$' \ @@ -126,14 +130,13 @@ jobs: - name: Checkout repository uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - name: Lint Python with Ruff + - name: Lint and format-check Python with Ruff shell: bash run: | + set -euo pipefail + tools_dir="$(bash .gitea/workflows/install-ci-tools.sh ruff)" + export PATH="$tools_dir:$PATH" ruff check . - - - name: Check Python formatting with Ruff - shell: bash - run: | ruff format --check . lint-yaml: @@ -146,6 +149,10 @@ jobs: - name: Lint YAML syntax shell: bash run: | + set -euo pipefail + tools_dir="$(bash .gitea/workflows/install-ci-tools.sh yamllint)" + export PATH="$tools_dir:$PATH" + mapfile -t yaml_files < <( git ls-files '*.yaml' '*.yml' \ ':!node_modules/**' \ @@ -169,6 +176,10 @@ jobs: - name: Lint Dockerfiles shell: bash run: | + set -euo pipefail + tools_dir="$(bash .gitea/workflows/install-ci-tools.sh hadolint)" + export PATH="$tools_dir:$PATH" + mapfile -t dockerfiles < <( git ls-files ':(glob)**/Dockerfile' ':(glob)**/Dockerfile.*' ) diff --git a/.gitea/workflows/install-ci-tools.sh b/.gitea/workflows/install-ci-tools.sh index 04873bc..f4565bf 100755 --- a/.gitea/workflows/install-ci-tools.sh +++ b/.gitea/workflows/install-ci-tools.sh @@ -19,15 +19,18 @@ 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). +# 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 @@ -107,6 +110,56 @@ install_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_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 @@ -122,7 +175,7 @@ install_actionlint() { wanted=("$@") if [ "${#wanted[@]}" -eq 0 ]; then - wanted=(kubeconform shellcheck actionlint) + wanted=(kubeconform shellcheck actionlint prettier ruff yamllint hadolint) fi for tool in "${wanted[@]}"; do @@ -130,6 +183,10 @@ for tool in "${wanted[@]}"; do 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 diff --git a/.gitea/workflows/tool-versions.env b/.gitea/workflows/tool-versions.env index ecc9926..e13a7db 100644 --- a/.gitea/workflows/tool-versions.env +++ b/.gitea/workflows/tool-versions.env @@ -1,11 +1,22 @@ # Pinned versions of the CI linters installed by install-ci-tools.sh. # Renovate keeps these up to date (see customManagers in renovate/renovate.json). # +# Every version below matches what was already installed on the runner, so +# pinning them changes what CI does not at all. It changes what CI does when +# the runner is rebuilt with something else: today install-ci-tools.sh finds +# the pinned version already on PATH and installs nothing, and a runner that +# drifts gets the pinned one installed over it. +# # The renovate image version is NOT pinned here: renovate/k8s/cronjob.yaml is the # single source of truth and the workflows read the tag from it, so there is # nothing to drift. ACTIONLINT_VERSION="1.7.7" SHELLCHECK_VERSION="0.11.0" KUBECONFORM_VERSION="0.8.0" -# uv builds the throwaway venv the pytest job runs in. +PRETTIER_VERSION="3.8.1" +RUFF_VERSION="0.16.8" +YAMLLINT_VERSION="1.38.0" +HADOLINT_VERSION="2.14.0" +# uv builds the throwaway venv the pytest job runs in, and unpacks the PyPI +# wheels for ruff and yamllint. UV_VERSION="0.12.17" diff --git a/renovate/k8s/configmap.yaml b/renovate/k8s/configmap.yaml index a30d0c5..6000964 100644 --- a/renovate/k8s/configmap.yaml +++ b/renovate/k8s/configmap.yaml @@ -101,6 +101,38 @@ data: "datasourceTemplate": "github-tags", "depNameTemplate": "astral-sh/uv" }, + { + "customType": "regex", + "description": "prettier version used by the ci workflow", + "managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"], + "matchStrings": ["(?:^|\\n)PRETTIER_VERSION=\"(?[0-9.]+)\""], + "datasourceTemplate": "npm", + "depNameTemplate": "prettier" + }, + { + "customType": "regex", + "description": "ruff version used by the ci workflow", + "managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"], + "matchStrings": ["(?:^|\\n)RUFF_VERSION=\"(?[0-9.]+)\""], + "datasourceTemplate": "pypi", + "depNameTemplate": "ruff" + }, + { + "customType": "regex", + "description": "yamllint version used by the ci workflow", + "managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"], + "matchStrings": ["(?:^|\\n)YAMLLINT_VERSION=\"(?[0-9.]+)\""], + "datasourceTemplate": "pypi", + "depNameTemplate": "yamllint" + }, + { + "customType": "regex", + "description": "hadolint version used by the ci workflow", + "managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"], + "matchStrings": ["(?:^|\\n)HADOLINT_VERSION=\"(?[0-9.]+)\""], + "datasourceTemplate": "github-tags", + "depNameTemplate": "hadolint/hadolint" + }, { "customType": "regex", "description": "stakater/reloader chart version pinned in the deploy workflow", diff --git a/renovate/renovate.json b/renovate/renovate.json index 9c7e490..d61c440 100644 --- a/renovate/renovate.json +++ b/renovate/renovate.json @@ -90,6 +90,38 @@ "datasourceTemplate": "github-tags", "depNameTemplate": "astral-sh/uv" }, + { + "customType": "regex", + "description": "prettier version used by the ci workflow", + "managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"], + "matchStrings": ["(?:^|\\n)PRETTIER_VERSION=\"(?[0-9.]+)\""], + "datasourceTemplate": "npm", + "depNameTemplate": "prettier" + }, + { + "customType": "regex", + "description": "ruff version used by the ci workflow", + "managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"], + "matchStrings": ["(?:^|\\n)RUFF_VERSION=\"(?[0-9.]+)\""], + "datasourceTemplate": "pypi", + "depNameTemplate": "ruff" + }, + { + "customType": "regex", + "description": "yamllint version used by the ci workflow", + "managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"], + "matchStrings": ["(?:^|\\n)YAMLLINT_VERSION=\"(?[0-9.]+)\""], + "datasourceTemplate": "pypi", + "depNameTemplate": "yamllint" + }, + { + "customType": "regex", + "description": "hadolint version used by the ci workflow", + "managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"], + "matchStrings": ["(?:^|\\n)HADOLINT_VERSION=\"(?[0-9.]+)\""], + "datasourceTemplate": "github-tags", + "depNameTemplate": "hadolint/hadolint" + }, { "customType": "regex", "description": "stakater/reloader chart version pinned in the deploy workflow",