diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index cf315f1..8367bb4 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -257,6 +257,12 @@ jobs: shell: bash run: | set -euo pipefail + # The pinned node, not whatever the runner has. Its system node is a + # rolling Arch package: during this very push its npm was missing + # entirely, and an hour later it was npm 12 on node 26. Both are the + # wrong major anyway — the panel image is node:22-alpine. + tools_dir="$(bash .gitea/workflows/install-ci-tools.sh node)" + export PATH="$tools_dir:$PATH" cd userbot/panel/frontend npm ci npm audit --omit=dev --audit-level=high @@ -286,8 +292,17 @@ jobs: # A venv in a temp dir rather than a checked-out one: the runner is # shared, and a leftover .venv would let a dependency the # requirements no longer pin still satisfy an import. + # + # --python is not optional. uv otherwise takes whatever interpreter it + # finds first, and which one that is depends on the machine: this + # runner runs jobs on the host, where the only interpreter is 3.14, + # and pyrogram's sync.py calls the bare asyncio.get_event_loop() that + # 3.14 no longer auto-creates, so three tests fail at collection. The + # image is python:3.13-slim, so 3.13 is also the version worth + # testing: uv fetches a managed build of it when the host has none, + # which is what makes this job independent of the runner. venv="$(mktemp -d)/venv" - uv venv --quiet "$venv" + uv venv --python 3.13 --quiet "$venv" uv pip install --quiet --python "$venv/bin/python" \ -r userbot/panel/backend/requirements-dev.txt @@ -314,6 +329,12 @@ jobs: shell: bash run: | set -euo pipefail + # The pinned node, not whatever the runner has. Its system node is a + # rolling Arch package: during this very push its npm was missing + # entirely, and an hour later it was npm 12 on node 26. Both are the + # wrong major anyway — the panel image is node:22-alpine. + tools_dir="$(bash .gitea/workflows/install-ci-tools.sh node)" + export PATH="$tools_dir:$PATH" cd userbot/panel/frontend npm ci diff --git a/.gitea/workflows/install-ci-tools.sh b/.gitea/workflows/install-ci-tools.sh index fc1de64..f1cdddf 100755 --- a/.gitea/workflows/install-ci-tools.sh +++ b/.gitea/workflows/install-ci-tools.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Installs the pinned CI linters into "$TOOLS_DIR/bin" and echoes that directory +# 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" @@ -19,17 +19,20 @@ 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). +# 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 ;; *) @@ -164,6 +167,32 @@ install_prettier() { 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 @@ -192,6 +221,7 @@ for tool in "${wanted[@]}"; do 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 diff --git a/.gitea/workflows/tool-versions.env b/.gitea/workflows/tool-versions.env index 286e74a..d010495 100644 --- a/.gitea/workflows/tool-versions.env +++ b/.gitea/workflows/tool-versions.env @@ -1,11 +1,11 @@ -# Pinned versions of the CI linters installed by install-ci-tools.sh. +# Pinned versions of the CI tools 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. +# Every version here except NODE_VERSION 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 @@ -24,3 +24,10 @@ PIP_AUDIT_VERSION="2.10.1" # uv builds the throwaway venv the pytest job runs in, and unpacks the PyPI # wheels for ruff, yamllint and pip-audit. UV_VERSION="0.12.17" +# node runs `npm ci` for the frontend tests and the npm audit, and it is the one +# pin here that does NOT come from the runner: the runner's system node is a +# rolling Arch package (it was node 26 with no npm at all when this was pinned), +# and the panel image is node:22-alpine. Pinned to the image's major on purpose, +# so the tree that gets tested is the tree that gets built. Renovate keeps this +# in step with the Dockerfile's node: tag via the "node runtime" group. +NODE_VERSION="22.23.3" diff --git a/renovate/k8s/configmap.yaml b/renovate/k8s/configmap.yaml index ffa3d4d..cb4e6bd 100644 --- a/renovate/k8s/configmap.yaml +++ b/renovate/k8s/configmap.yaml @@ -141,6 +141,14 @@ data: "datasourceTemplate": "github-tags", "depNameTemplate": "hadolint/hadolint" }, + { + "customType": "regex", + "description": "node version the ci workflow runs npm with", + "managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"], + "matchStrings": ["(?:^|\\n)NODE_VERSION=\"(?[0-9.]+)\""], + "datasourceTemplate": "node", + "depNameTemplate": "node" + }, { "customType": "regex", "description": "stakater/reloader chart version pinned in the deploy workflow", @@ -175,6 +183,13 @@ data: "groupName": "renovate self-update", "automerge": false }, + { + "description": "CI runs npm on the node the panel image is built from - the NODE_VERSION pin in tool-versions.env and node:22-alpine in the Dockerfile are the same dependency and move as one", + "matchPackageNames": ["node"], + "groupName": "node runtime", + "groupSlug": "node", + "automerge": false + }, { "description": "Helm chart bumps change PVC fields and admission behaviour, keep them reviewable", "matchDatasources": ["helm"], diff --git a/renovate/renovate.json b/renovate/renovate.json index 3c7a06b..d52898a 100644 --- a/renovate/renovate.json +++ b/renovate/renovate.json @@ -130,6 +130,14 @@ "datasourceTemplate": "github-tags", "depNameTemplate": "hadolint/hadolint" }, + { + "customType": "regex", + "description": "node version the ci workflow runs npm with", + "managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"], + "matchStrings": ["(?:^|\\n)NODE_VERSION=\"(?[0-9.]+)\""], + "datasourceTemplate": "node", + "depNameTemplate": "node" + }, { "customType": "regex", "description": "stakater/reloader chart version pinned in the deploy workflow", @@ -164,6 +172,13 @@ "groupName": "renovate self-update", "automerge": false }, + { + "description": "CI runs npm on the node the panel image is built from - the NODE_VERSION pin in tool-versions.env and node:22-alpine in the Dockerfile are the same dependency and move as one", + "matchPackageNames": ["node"], + "groupName": "node runtime", + "groupSlug": "node", + "automerge": false + }, { "description": "Helm chart bumps change PVC fields and admission behaviour, keep them reviewable", "matchDatasources": ["helm"],