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",