From 3db1b0676fea0c66501366fa0bde41dc0b83376d Mon Sep 17 00:00:00 2001 From: copilotcoder Date: Wed, 22 Jul 2026 17:38:34 -0400 Subject: [PATCH] chore(ci): centralize runtime and image version constants --- .gitea/workflows/cicd.yaml | 68 +++++++++++++++---- Dockerfile.cicd-base | 48 ++++++++----- Dockerfile.e2e-tester | 4 +- Dockerfile.frontend | 8 ++- .../gitea-actions/repair_runner_mirror.xsh | 27 +++++++- scripts/run-ci-e2e-compose.sh | 13 +++- versions/ci.env | 18 +++++ 7 files changed, 150 insertions(+), 36 deletions(-) create mode 100644 versions/ci.env diff --git a/.gitea/workflows/cicd.yaml b/.gitea/workflows/cicd.yaml index f18d828..4b56588 100644 --- a/.gitea/workflows/cicd.yaml +++ b/.gitea/workflows/cicd.yaml @@ -135,9 +135,9 @@ jobs: if retry_cmd 5 3 env GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" \ git fetch --depth 1 origin "${HEAD_SHA}" >/dev/null 2>&1; then - git checkout FETCH_HEAD -- Dockerfile.cicd-base .dockerignore scripts/compute-cicd-base-hash.sh scripts/resolve-mirrored-image.sh + git checkout FETCH_HEAD -- Dockerfile.cicd-base .dockerignore versions/ci.env scripts/compute-cicd-base-hash.sh scripts/resolve-mirrored-image.sh else - git checkout HEAD -- Dockerfile.cicd-base .dockerignore scripts/compute-cicd-base-hash.sh scripts/resolve-mirrored-image.sh + git checkout HEAD -- Dockerfile.cicd-base .dockerignore versions/ci.env scripts/compute-cicd-base-hash.sh scripts/resolve-mirrored-image.sh fi chmod +x scripts/compute-cicd-base-hash.sh @@ -324,8 +324,13 @@ jobs: docker_login_with_retry 5 3 - PLAYWRIGHT_BROWSERS_MIRROR_TAG="${GITEA_REGISTRY}/darkhelm.org/playwright-browsers:v1.61.1-jammy" - PLAYWRIGHT_BROWSERS_UPSTREAM_TAG="mcr.microsoft.com/playwright:v1.61.1-jammy" + set -a + # shellcheck source=versions/ci.env + source versions/ci.env + set +a + + PLAYWRIGHT_BROWSERS_MIRROR_TAG="${GITEA_REGISTRY}/${PLAYWRIGHT_MIRROR_IMAGE}" + PLAYWRIGHT_BROWSERS_UPSTREAM_TAG="${PLAYWRIGHT_IMAGE}" PLAYWRIGHT_BROWSERS_LOCAL_TAG="$(./scripts/resolve-mirrored-image.sh \ --image "${PLAYWRIGHT_BROWSERS_MIRROR_TAG}" \ --mirror-image "${PLAYWRIGHT_BROWSERS_MIRROR_TAG}" \ @@ -342,6 +347,8 @@ jobs: docker build --progress=plain -f Dockerfile.cicd-base \ --build-arg BASE_IMAGE_VERSION="v1.0.0-${BASE_HASH}" \ --build-arg BASE_IMAGE_HASH="${BASE_HASH}" \ + --build-arg PLAYWRIGHT_VERSION="${PLAYWRIGHT_VERSION}" \ + --build-arg PLAYWRIGHT_DISTRO="${PLAYWRIGHT_DISTRO}" \ --build-arg PLAYWRIGHT_BROWSERS_IMAGE="${PLAYWRIGHT_BROWSERS_IMAGE}" \ -t cicd-base:latest . @@ -399,9 +406,9 @@ jobs: if retry_cmd 5 3 env GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" \ git fetch --depth 1 origin "${HEAD_SHA}" >/dev/null 2>&1; then - git checkout FETCH_HEAD -- .dockerignore Dockerfile.cicd Dockerfile.cicd-base scripts/compute-cicd-base-hash.sh scripts/resolve-mirrored-image.sh + git checkout FETCH_HEAD -- .dockerignore Dockerfile.cicd Dockerfile.cicd-base versions/ci.env scripts/compute-cicd-base-hash.sh scripts/resolve-mirrored-image.sh else - git checkout HEAD -- .dockerignore Dockerfile.cicd Dockerfile.cicd-base scripts/compute-cicd-base-hash.sh scripts/resolve-mirrored-image.sh + git checkout HEAD -- .dockerignore Dockerfile.cicd Dockerfile.cicd-base versions/ci.env scripts/compute-cicd-base-hash.sh scripts/resolve-mirrored-image.sh fi chmod +x scripts/compute-cicd-base-hash.sh chmod +x scripts/resolve-mirrored-image.sh @@ -1094,11 +1101,18 @@ jobs: FRONTEND_BASE_TAG_REF="${FRONTEND_BASE_REPO}:${HEAD_SHA}" FRONTEND_BASE_CACHE_REF="${FRONTEND_BASE_REPO}:cache" + set -a + # shellcheck source=versions/ci.env + source versions/ci.env + set +a + retry_registry_op pull "${FRONTEND_BASE_CACHE_REF}" 5 3 || true BUILDKIT_NO_CLIENT_TOKEN=1 DOCKER_BUILDKIT=1 docker build -f Dockerfile.frontend \ --target deps \ --cache-from "${FRONTEND_BASE_CACHE_REF}" \ --build-arg BUILDKIT_INLINE_CACHE=1 \ + --build-arg NODE_ALPINE_TAG="${FRONTEND_NODE_ALPINE_TAG}" \ + --build-arg YARN_VERSION="${FRONTEND_YARN_VERSION}" \ -t plex-playlist-frontend-base:"${HEAD_SHA}" . docker tag plex-playlist-frontend-base:"${HEAD_SHA}" "${FRONTEND_BASE_TAG_REF}" docker tag plex-playlist-frontend-base:"${HEAD_SHA}" "${FRONTEND_BASE_CACHE_REF}" @@ -1285,12 +1299,19 @@ jobs: DEPLOYABLE_FRONTEND_TAG_REF="${DEPLOYABLE_FRONTEND_REPO}:${HEAD_SHA}" DEPLOYABLE_FRONTEND_CACHE_REF="${DEPLOYABLE_FRONTEND_REPO}:cache" + set -a + # shellcheck source=versions/ci.env + source versions/ci.env + set +a + docker_login_with_retry 5 3 retry_registry_op pull "${DEPLOYABLE_FRONTEND_CACHE_REF}" 5 3 || true BUILDKIT_NO_CLIENT_TOKEN=1 DOCKER_BUILDKIT=1 docker build -f Dockerfile.frontend \ --target production \ --cache-from "${DEPLOYABLE_FRONTEND_CACHE_REF}" \ --build-arg BUILDKIT_INLINE_CACHE=1 \ + --build-arg NODE_ALPINE_TAG="${FRONTEND_NODE_ALPINE_TAG}" \ + --build-arg YARN_VERSION="${FRONTEND_YARN_VERSION}" \ -t plex-playlist-frontend:"${HEAD_SHA}" . bash ./scripts/verify-deployable-image-purity.sh --image plex-playlist-frontend:"${HEAD_SHA}" --profile frontend @@ -1442,8 +1463,13 @@ jobs: run: | set -euo pipefail - PLAYWRIGHT_BASE_IMAGE_MIRROR="${GITEA_REGISTRY}/darkhelm.org/playwright-browsers:v1.61.1-jammy" - PLAYWRIGHT_BASE_IMAGE_UPSTREAM="mcr.microsoft.com/playwright:v1.61.1-jammy" + set -a + # shellcheck source=versions/ci.env + source versions/ci.env + set +a + + PLAYWRIGHT_BASE_IMAGE_MIRROR="${GITEA_REGISTRY}/${PLAYWRIGHT_MIRROR_IMAGE}" + PLAYWRIGHT_BASE_IMAGE_UPSTREAM="${PLAYWRIGHT_IMAGE}" docker_login_with_retry() { attempts="${1:-5}" @@ -1543,6 +1569,8 @@ jobs: DOCKER_BUILDKIT=1 docker build --pull=false -f Dockerfile.e2e-tester \ --cache-from "${E2E_TESTER_CACHE_REF}" \ --build-arg BUILDKIT_INLINE_CACHE=1 \ + --build-arg PLAYWRIGHT_VERSION="${PLAYWRIGHT_VERSION}" \ + --build-arg PLAYWRIGHT_DISTRO="${PLAYWRIGHT_DISTRO}" \ --build-arg PLAYWRIGHT_BASE_IMAGE="${PLAYWRIGHT_BASE_IMAGE}" \ -t plex-playlist-e2e:"${HEAD_SHA}" . docker tag plex-playlist-e2e:"${HEAD_SHA}" "${E2E_TESTER_TAG_REF}" @@ -1806,8 +1834,7 @@ jobs: COMPOSE_PROJECT_NAME="plex-int-${RUN_ID}-${RUN_ATTEMPT}" DB_PASSWORD="plex_password" DB_URL="postgresql://plex_user:${DB_PASSWORD}@database:5432/plex_playlist" - POSTGRES_IMAGE_MIRROR="${GITEA_REGISTRY}/darkhelm.org/postgres:16-alpine" - POSTGRES_IMAGE_UPSTREAM="docker.io/library/postgres:16-alpine" + VERSION_ENV_PATH="/tmp/ci.env" RESOLVER_PATH="/tmp/resolve-mirrored-image.sh" if ! command -v docker >/dev/null 2>&1; then @@ -1839,9 +1866,18 @@ jobs: RESOLVER_SOURCE_IMAGE="${GITEA_REGISTRY}/darkhelm.org/plex-playlist-cicd:${HEAD_SHA}" RESOLVER_CONTAINER="$(docker create "${RESOLVER_SOURCE_IMAGE}")" docker cp "${RESOLVER_CONTAINER}:/workspace/scripts/resolve-mirrored-image.sh" "${RESOLVER_PATH}" + docker cp "${RESOLVER_CONTAINER}:/workspace/versions/ci.env" "${VERSION_ENV_PATH}" docker rm -f "${RESOLVER_CONTAINER}" >/dev/null 2>&1 || true chmod +x "${RESOLVER_PATH}" + set -a + # shellcheck source=/tmp/ci.env + source "${VERSION_ENV_PATH}" + set +a + + POSTGRES_IMAGE_MIRROR="${GITEA_REGISTRY}/${POSTGRES_MIRROR_IMAGE}" + POSTGRES_IMAGE_UPSTREAM="${POSTGRES_UPSTREAM_IMAGE}" + POSTGRES_IMAGE="$(${RESOLVER_PATH} \ --image "${POSTGRES_IMAGE_MIRROR}" \ --mirror-image "${POSTGRES_IMAGE_MIRROR}" \ @@ -2113,8 +2149,7 @@ jobs: DB_PASSWORD="plex_password" DB_URL="postgresql://plex_user:${DB_PASSWORD}@database:5432/plex_playlist" E2E_REPO_DIR="$(mktemp -d)" - POSTGRES_IMAGE_MIRROR="${GITEA_REGISTRY}/darkhelm.org/postgres:16-alpine" - POSTGRES_IMAGE_UPSTREAM="docker.io/library/postgres:16-alpine" + VERSION_ENV_PATH="/tmp/ci.env" RESOLVER_PATH="/tmp/resolve-mirrored-image.sh" if ! command -v docker >/dev/null 2>&1; then @@ -2146,9 +2181,18 @@ jobs: RESOLVER_SOURCE_IMAGE="${GITEA_REGISTRY}/darkhelm.org/plex-playlist-cicd:${HEAD_SHA}" RESOLVER_CONTAINER="$(docker create "${RESOLVER_SOURCE_IMAGE}")" docker cp "${RESOLVER_CONTAINER}:/workspace/scripts/resolve-mirrored-image.sh" "${RESOLVER_PATH}" + docker cp "${RESOLVER_CONTAINER}:/workspace/versions/ci.env" "${VERSION_ENV_PATH}" docker rm -f "${RESOLVER_CONTAINER}" >/dev/null 2>&1 || true chmod +x "${RESOLVER_PATH}" + set -a + # shellcheck source=/tmp/ci.env + source "${VERSION_ENV_PATH}" + set +a + + POSTGRES_IMAGE_MIRROR="${GITEA_REGISTRY}/${POSTGRES_MIRROR_IMAGE}" + POSTGRES_IMAGE_UPSTREAM="${POSTGRES_UPSTREAM_IMAGE}" + POSTGRES_IMAGE="$(${RESOLVER_PATH} \ --image "${POSTGRES_IMAGE_MIRROR}" \ --mirror-image "${POSTGRES_IMAGE_MIRROR}" \ diff --git a/Dockerfile.cicd-base b/Dockerfile.cicd-base index 2a3fba8..83bcc34 100644 --- a/Dockerfile.cicd-base +++ b/Dockerfile.cicd-base @@ -1,8 +1,20 @@ # CICD Base Setup - System dependencies and language runtimes only -ARG PLAYWRIGHT_BROWSERS_IMAGE=mcr.microsoft.com/playwright:v1.61.1-jammy +ARG PLAYWRIGHT_VERSION=1.61.1 +ARG PLAYWRIGHT_DISTRO=jammy +ARG PLAYWRIGHT_BROWSERS_IMAGE=mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-${PLAYWRIGHT_DISTRO} FROM ${PLAYWRIGHT_BROWSERS_IMAGE} AS playwright-browsers -FROM ubuntu:26.04 +ARG UBUNTU_VERSION=26.04 +FROM ubuntu:${UBUNTU_VERSION} + +ARG PLAYWRIGHT_VERSION +ARG PYTHON_VERSION=3.14 +ARG NODE_MAJOR=24 +ARG TYPESCRIPT_VERSION=5.3.3 +ARG ESLINT_VERSION=9.33.0 +ARG PRETTIER_VERSION=3.6.2 +ARG VITE_VERSION=7.1.10 +ARG NODE_TYPES_VERSION=20.16.0 # Build args for cache busting (base dependencies change rarely) ARG BASE_IMAGE_VERSION=v1.0.0 @@ -88,9 +100,9 @@ RUN set -eux; \ wget \ && rm -rf /var/lib/apt/lists/* -# Install Python 3.14 with retry and fallback mechanisms +# Install Python with retry and fallback mechanisms RUN for i in 1 2 3; do \ - echo "Attempt $i: Adding deadsnakes PPA..." && \ + echo "Attempt $i: Adding deadsnakes PPA..." && \ add-apt-repository -y ppa:deadsnakes/ppa && \ find /etc/apt -type f \( -name 'sources.list' -o -name '*.sources' -o -name '*.list' \) -print0 | xargs -0 sed -i 's|http://|https://|g' && \ apt-get update && \ @@ -99,21 +111,21 @@ RUN for i in 1 2 3; do \ done RUN for i in 1 2 3; do \ - echo "Attempt $i: Installing Python 3.14..." && \ + echo "Attempt $i: Installing Python ${PYTHON_VERSION}..." && \ timeout 300 apt-get install -y --no-install-recommends \ - python3.14 \ - python3.14-venv \ - python3.14-dev && \ + python${PYTHON_VERSION} \ + python${PYTHON_VERSION}-venv \ + python${PYTHON_VERSION}-dev && \ break || \ (echo "Attempt $i failed, retrying in 15s..." && sleep 15); \ done && \ rm -rf /var/lib/apt/lists/* -# Install Node.js 24 with retry mechanism +# Install Node.js with retry mechanism RUN for i in 1 2 3; do \ - echo "Attempt $i: Installing Node.js 24..." && \ + echo "Attempt $i: Installing Node.js ${NODE_MAJOR}..." && \ curl -fsSL --connect-timeout 30 --max-time 300 \ - https://deb.nodesource.com/setup_24.x | bash - && \ + https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - && \ find /etc/apt -type f \( -name 'sources.list' -o -name '*.sources' -o -name '*.list' \) -print0 | xargs -0 sed -i 's|http://|https://|g' && \ apt-get update && \ timeout 300 apt-get install -y --no-install-recommends nodejs && \ @@ -146,12 +158,12 @@ RUN set -eu && \ RUN echo "=== Installing Global Development Tools ===" && \ # Use npm for global installations (works better than yarn global in Berry) npm install -g \ - @playwright/test@1.56.1 \ - typescript@5.3.3 \ - eslint@9.33.0 \ - prettier@3.6.2 \ - vite@7.1.10 \ - @types/node@20.16.0 && \ + @playwright/test@${PLAYWRIGHT_VERSION} \ + typescript@${TYPESCRIPT_VERSION} \ + eslint@${ESLINT_VERSION} \ + prettier@${PRETTIER_VERSION} \ + vite@${VITE_VERSION} \ + @types/node@${NODE_TYPES_VERSION} && \ # Verify global tools are available which playwright && \ which tsc && \ @@ -212,7 +224,7 @@ RUN echo '#!/bin/bash' > /usr/local/bin/setup-ssh && \ # Verify that all base tools are working RUN echo "=== Base System Verification ===" && \ - python3.14 --version && \ + python${PYTHON_VERSION} --version && \ node --version && \ yarn --version && \ uv --version && \ diff --git a/Dockerfile.e2e-tester b/Dockerfile.e2e-tester index c40ef20..96ffbf7 100644 --- a/Dockerfile.e2e-tester +++ b/Dockerfile.e2e-tester @@ -1,4 +1,6 @@ -ARG PLAYWRIGHT_BASE_IMAGE=mcr.microsoft.com/playwright:v1.61.1-jammy +ARG PLAYWRIGHT_VERSION=1.61.1 +ARG PLAYWRIGHT_DISTRO=jammy +ARG PLAYWRIGHT_BASE_IMAGE=mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-${PLAYWRIGHT_DISTRO} FROM ${PLAYWRIGHT_BASE_IMAGE} WORKDIR /workspace/frontend diff --git a/Dockerfile.frontend b/Dockerfile.frontend index 3ca32a7..e1330c6 100644 --- a/Dockerfile.frontend +++ b/Dockerfile.frontend @@ -1,5 +1,9 @@ # Frontend Dockerfile for Vue/Vite TypeScript project -FROM node:24-alpine AS deps +ARG NODE_ALPINE_TAG=24-alpine +ARG YARN_VERSION=4.17.1 +FROM node:${NODE_ALPINE_TAG} AS deps + +ARG YARN_VERSION WORKDIR /app @@ -20,7 +24,7 @@ RUN set -eux; \ return 1; \ }; \ corepack enable; \ - retry 5 corepack prepare yarn@4.17.1 --activate; \ + retry 5 corepack prepare yarn@${YARN_VERSION} --activate; \ retry 5 yarn install --immutable FROM deps AS build diff --git a/scripts/gitea-actions/repair_runner_mirror.xsh b/scripts/gitea-actions/repair_runner_mirror.xsh index 422e4cb..aa039dc 100755 --- a/scripts/gitea-actions/repair_runner_mirror.xsh +++ b/scripts/gitea-actions/repair_runner_mirror.xsh @@ -1,5 +1,23 @@ #!/usr/bin/env xonsh +from pathlib import Path + + +def load_ci_versions(): + env_path = Path(__file__).resolve().parents[2] / "versions" / "ci.env" + values = {} + for raw_line in env_path.read_text().splitlines(): + line = raw_line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + values[key.strip()] = value.strip() + return values + + +ci_versions = load_ci_versions() +playwright_tag = ci_versions["PLAYWRIGHT_IMAGE_TAG"] + hosts = [ "kankali.darkhelm.lan", "zhokq.darkhelm.lan", @@ -14,7 +32,7 @@ remote_code_template = """ mirror_pairs = [ ("ACT_UBUNTU", "ghcr.io/catthehacker/ubuntu:act-latest", "kankali.darkhelm.lan:3001/darkhelm.org/act-ubuntu:act-latest"), ("RENOVATE", "ghcr.io/renovatebot/renovate:41", "kankali.darkhelm.lan:3001/darkhelm.org/renovate:41"), - ("PLAYWRIGHT", "mcr.microsoft.com/playwright:v1.61.1-jammy", "kankali.darkhelm.lan:3001/darkhelm.org/playwright-browsers:v1.61.1-jammy"), + ("PLAYWRIGHT", "mcr.microsoft.com/playwright:" + __PLAYWRIGHT_TAG__, "kankali.darkhelm.lan:3001/darkhelm.org/playwright-browsers:" + __PLAYWRIGHT_TAG__), ] is_publisher = __IS_PUBLISHER__ @@ -94,5 +112,10 @@ for label, source_image, mirror_image in mirror_pairs: for host in hosts: print(f"\n=== {host} ===") - remote_code = remote_code_template.replace("__IS_PUBLISHER__", "True" if host == publisher_host else "False") + quoted_playwright_tag = repr(playwright_tag) + remote_code = ( + remote_code_template + .replace("__IS_PUBLISHER__", "True" if host == publisher_host else "False") + .replace("__PLAYWRIGHT_TAG__", quoted_playwright_tag) + ) ssh @(host) @(remote_code) diff --git a/scripts/run-ci-e2e-compose.sh b/scripts/run-ci-e2e-compose.sh index f39a0c7..6ed457c 100644 --- a/scripts/run-ci-e2e-compose.sh +++ b/scripts/run-ci-e2e-compose.sh @@ -4,6 +4,17 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" COMPOSE_FILE="${ROOT_DIR}/compose.ci.e2e.yml" PROJECT_NAME="${COMPOSE_PROJECT_NAME:-plex-e2e-local}" +VERSION_ENV_FILE="${ROOT_DIR}/versions/ci.env" + +if [ ! -f "${VERSION_ENV_FILE}" ]; then + echo "❌ Missing version source file: ${VERSION_ENV_FILE}" + exit 1 +fi + +set -a +# shellcheck source=../versions/ci.env +source "${VERSION_ENV_FILE}" +set +a mkdir -p "${ROOT_DIR}/frontend/.playwright-artifacts" @@ -12,7 +23,7 @@ export DEPLOYABLE_FRONTEND_IMAGE="${DEPLOYABLE_FRONTEND_IMAGE:-plex-playlist-fro export DB_PASSWORD="${DB_PASSWORD:-plex_password}" export DB_URL="${DB_URL:-postgresql://plex_user:${DB_PASSWORD}@database:5432/plex_playlist}" export E2E_ARTIFACT_DIR="${E2E_ARTIFACT_DIR:-${ROOT_DIR}/frontend/.playwright-artifacts}" -export PLAYWRIGHT_E2E_IMAGE="${PLAYWRIGHT_E2E_IMAGE:-mcr.microsoft.com/playwright:v1.61.1-jammy}" +export PLAYWRIGHT_E2E_IMAGE="${PLAYWRIGHT_E2E_IMAGE:-${PLAYWRIGHT_IMAGE}}" cleanup() { docker compose -p "${PROJECT_NAME}" -f "${COMPOSE_FILE}" down -v --remove-orphans >/dev/null 2>&1 || true diff --git a/versions/ci.env b/versions/ci.env new file mode 100644 index 0000000..f1858c2 --- /dev/null +++ b/versions/ci.env @@ -0,0 +1,18 @@ +# CI/runtime version source of truth for scripts and docker build args. +# shellcheck disable=SC2034 +PLAYWRIGHT_VERSION=1.61.1 +PLAYWRIGHT_DISTRO=jammy +PLAYWRIGHT_IMAGE_TAG=v1.61.1-jammy +PLAYWRIGHT_IMAGE=mcr.microsoft.com/playwright:${PLAYWRIGHT_IMAGE_TAG} +PLAYWRIGHT_MIRROR_IMAGE=darkhelm.org/playwright-browsers:${PLAYWRIGHT_IMAGE_TAG} + +POSTGRES_IMAGE_TAG=16-alpine +POSTGRES_MIRROR_IMAGE=darkhelm.org/postgres:${POSTGRES_IMAGE_TAG} +POSTGRES_UPSTREAM_IMAGE=docker.io/library/postgres:${POSTGRES_IMAGE_TAG} + +FRONTEND_NODE_ALPINE_TAG=24-alpine +FRONTEND_YARN_VERSION=4.17.1 + +UBUNTU_CICD_BASE_VERSION=26.04 +PYTHON_VERSION=3.14 +NODE_MAJOR=24