diff --git a/.gitattributes b/.gitattributes index b749c3eb8556..0d2714112671 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,7 +14,6 @@ Dockerfile.dev linguist-language=Dockerfile # Generated files CODEOWNERS linguist-generated=true -Dockerfile linguist-generated=true homeassistant/generated/*.py linguist-generated=true machine/* linguist-generated=true mypy.ini linguist-generated=true diff --git a/.github/renovate.json b/.github/renovate.json index 93c1cbf2372b..b3c915170f96 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -6,6 +6,7 @@ "pep621", "pip_requirements", "pre-commit", + "dockerfile", "custom.regex", "homeassistant-manifest" ], @@ -21,6 +22,10 @@ ] }, + "dockerfile": { + "managerFilePatterns": ["/^Dockerfile$/"] + }, + "homeassistant-manifest": { "managerFilePatterns": [ "/^homeassistant/components/[^/]+/manifest\\.json$/" @@ -35,6 +40,14 @@ "matchStrings": ["required-version = \">=(?[\\d.]+)\""], "depNameTemplate": "ruff", "datasourceTemplate": "pypi" + }, + { + "customType": "regex", + "description": "Update go2rtc RECOMMENDED_VERSION in const.py alongside the Dockerfile pin", + "managerFilePatterns": ["/^homeassistant/components/go2rtc/const\\.py$/"], + "matchStrings": ["RECOMMENDED_VERSION = \"(?[\\d.]+)\""], + "depNameTemplate": "ghcr.io/alexxit/go2rtc", + "datasourceTemplate": "docker" } ], @@ -184,6 +197,13 @@ "enabled": true, "labels": ["dependency"] }, + { + "description": "Docker allowlist (ghcr.io exposes no release timestamps so the global cooldown needs to be bypassed)", + "matchPackageNames": ["ghcr.io/alexxit/go2rtc"], + "enabled": true, + "minimumReleaseAge": null, + "labels": ["dependency"] + }, { "description": "Group ruff pre-commit hook with its PyPI twin into one PR", "matchPackageNames": ["astral-sh/ruff-pre-commit", "ruff"], @@ -213,6 +233,12 @@ "matchPackageNames": ["pylint", "astroid"], "groupName": "pylint", "groupSlug": "pylint" + }, + { + "description": "Group go2rtc Dockerfile pin with const.py RECOMMENDED_VERSION into one PR", + "matchPackageNames": ["ghcr.io/alexxit/go2rtc"], + "groupName": "go2rtc", + "groupSlug": "go2rtc" } ] } diff --git a/Dockerfile b/Dockerfile index 5ce38cd16566..2e4cf7cb7521 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile@sha256:2780b5c3bab67f1f76c781860de469442999ed1a0d7992a5efdf2cffc0e3d769 -# Automatically generated by hassfest. +# Partly generated by hassfest. # # To update, run python3 -m script.hassfest -p docker ARG BUILD_FROM @@ -26,7 +26,7 @@ WORKDIR /usr/src COPY rootfs / # Add go2rtc binary -COPY --from=ghcr.io/alexxit/go2rtc@sha256:675c318b23c06fd862a61d262240c9a63436b4050d177ffc68a32710d9e05bae /usr/local/bin/go2rtc /bin/go2rtc +COPY --from=ghcr.io/alexxit/go2rtc:1.9.14@sha256:675c318b23c06fd862a61d262240c9a63436b4050d177ffc68a32710d9e05bae /usr/local/bin/go2rtc /bin/go2rtc ## Setup Home Assistant Core dependencies COPY --parents requirements.txt homeassistant/package_constraints.txt homeassistant/ diff --git a/homeassistant/components/go2rtc/const.py b/homeassistant/components/go2rtc/const.py index 55316f71fff5..6b11322cfe9b 100644 --- a/homeassistant/components/go2rtc/const.py +++ b/homeassistant/components/go2rtc/const.py @@ -6,6 +6,5 @@ CONF_DEBUG_UI = "debug_ui" DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time." HA_MANAGED_API_PORT = 11984 HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/" -# When changing this version, also update the corresponding SHA hash (_GO2RTC_SHA) -# in script/hassfest/docker.py. +# Kept in sync with the go2rtc image pinned in the root Dockerfile by Renovate. RECOMMENDED_VERSION = "1.9.14" diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py index 057d09fd772e..cd3360541769 100644 --- a/script/hassfest/docker.py +++ b/script/hassfest/docker.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from pathlib import Path +import re from homeassistant import core from homeassistant.util import executor, thread @@ -13,70 +14,20 @@ _DOCKERFILE_SYNTAX_SHA = ( "2780b5c3bab67f1f76c781860de469442999ed1a0d7992a5efdf2cffc0e3d769" # 1.23.0 ) -_GO2RTC_SHA = ( - "675c318b23c06fd862a61d262240c9a63436b4050d177ffc68a32710d9e05bae" # 1.9.14 -) +_DOCKERFILE_SYNTAX_PATTERN = re.compile(r"# syntax=docker/dockerfile@sha256:[0-9a-f]+") +_DOCKERFILE_S6_GRACETIME_PATTERN = re.compile(r"S6_SERVICES_GRACETIME=\d+") -DOCKERFILE_TEMPLATE = r"""# syntax=docker/dockerfile@sha256:{dockerfile_syntax} -# Automatically generated by hassfest. -# -# To update, run python3 -m script.hassfest -p docker -ARG BUILD_FROM -FROM ${{BUILD_FROM}} -LABEL \ - io.hass.type="core" \ - org.opencontainers.image.authors="The Home Assistant Authors" \ - org.opencontainers.image.description="Open-source home automation platform running on Python 3" \ - org.opencontainers.image.documentation="https://www.home-assistant.io/docs/" \ - org.opencontainers.image.licenses="Apache-2.0" \ - org.opencontainers.image.title="Home Assistant" \ - org.opencontainers.image.url="https://www.home-assistant.io/" - -# Synchronize with homeassistant/core.py:async_stop -ENV \ - S6_SERVICES_GRACETIME={timeout} \ - UV_SYSTEM_PYTHON=true \ - UV_NO_CACHE=true - -WORKDIR /usr/src - -# Home Assistant S6-Overlay -COPY rootfs / - -# Add go2rtc binary -COPY --from=ghcr.io/alexxit/go2rtc@sha256:{go2rtc} /usr/local/bin/go2rtc /bin/go2rtc - -## Setup Home Assistant Core dependencies -COPY --parents requirements.txt homeassistant/package_constraints.txt homeassistant/ -RUN \ - # Verify go2rtc can be executed - go2rtc --version \ - # Install uv at the version pinned in the requirements file - && pip3 install --no-cache-dir "uv==$(awk -F'==' '/^uv==/{{print $2}}' homeassistant/requirements.txt)" \ - && uv pip install \ - --no-build \ - -r homeassistant/requirements.txt - -COPY requirements_all.txt home_assistant_frontend-* home_assistant_intents-* homeassistant/ -RUN \ - if ls homeassistant/home_assistant_*.whl 1> /dev/null 2>&1; then \ - uv pip install homeassistant/home_assistant_*.whl; \ - fi \ - && uv pip install \ - --no-build \ - -r homeassistant/requirements_all.txt - -## Setup Home Assistant Core -COPY --parents LICENSE* README* homeassistant/ pyproject.toml homeassistant/ -RUN \ - uv pip install \ - -e ./homeassistant \ - && python3 -m compileall \ - homeassistant/homeassistant - -WORKDIR /config -""" +def _update_dockerfile(content: str, timeout: int) -> str: + """Update the hassfest-managed parts of the Dockerfile.""" + content = _DOCKERFILE_SYNTAX_PATTERN.sub( + f"# syntax=docker/dockerfile@sha256:{_DOCKERFILE_SYNTAX_SHA}", + content, + count=1, + ) + return _DOCKERFILE_S6_GRACETIME_PATTERN.sub( + f"S6_SERVICES_GRACETIME={timeout}", content, count=1 + ) @dataclass(frozen=True) @@ -199,14 +150,12 @@ def _generate_files(config: Config) -> list[File]: + 10 ) * 1000 + dockerfile_path = config.root / "Dockerfile" + files = [ File( - DOCKERFILE_TEMPLATE.format( - dockerfile_syntax=_DOCKERFILE_SYNTAX_SHA, - timeout=timeout, - go2rtc=_GO2RTC_SHA, - ), - config.root / "Dockerfile", + _update_dockerfile(dockerfile_path.read_text(encoding="UTF-8"), timeout), + dockerfile_path, ), File( _HASSFEST_TEMPLATE.format( diff --git a/tests/components/go2rtc/test_docker_version.py b/tests/components/go2rtc/test_docker_version.py deleted file mode 100644 index 456ec344418c..000000000000 --- a/tests/components/go2rtc/test_docker_version.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Test that the go2rtc Docker image version matches or exceeds the recommended version. - -This test ensures that the go2rtc Docker image SHA pinned in -script/hassfest/docker.py corresponds to a version that is equal to or -greater than the RECOMMENDED_VERSION defined in -homeassistant/components/go2rtc/const.py. - -The test pulls the Docker image using the pinned SHA and runs the -`go2rtc --version` command inside the container to extract the version, -then compares it against RECOMMENDED_VERSION. -""" - -import asyncio -import os -import re - -from awesomeversion import AwesomeVersion -import pytest - -from homeassistant.components.go2rtc.const import RECOMMENDED_VERSION -from script.hassfest.docker import _GO2RTC_SHA as DOCKER_SHA - - -async def _get_version_from_docker_sha() -> str: - """Extract go2rtc version from Docker image using the pinned SHA.""" - - image = f"ghcr.io/alexxit/go2rtc@sha256:{DOCKER_SHA}" - - pull_process = await asyncio.create_subprocess_exec( - "docker", - "pull", - image, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - _, pull_stderr = await pull_process.communicate() - - if pull_process.returncode != 0: - raise RuntimeError(f"Failed to pull go2rtc image: {pull_stderr.decode()}") - - # Run the container to get version - run_process = await asyncio.create_subprocess_exec( - "docker", - "run", - "--rm", - image, - "go2rtc", - "--version", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - run_stdout, run_stderr = await run_process.communicate() - - if run_process.returncode != 0: - raise RuntimeError(f"Failed to run go2rtc --version: {run_stderr.decode()}") - - # Parse version from output - # Expected output format: "go2rtc version 1.9.12 (commit) linux/amd64" or similar - output = run_stdout.decode().strip() - version_match = re.search(r"version\s+([\d.]+)", output) - - if not version_match: - raise RuntimeError(f"Could not parse version from go2rtc output: {output}") - - return version_match.group(1) - - -@pytest.mark.skipif( - not os.environ.get("CI"), - reason="This test requires Docker and only runs in CI", -) -async def test_docker_version_matches_recommended() -> None: - """Test go2rtc Docker SHA version meets RECOMMENDED_VERSION.""" - # Extract version from the actual Docker container - docker_version_str = await _get_version_from_docker_sha() - - # Parse versions - docker_version = AwesomeVersion(docker_version_str) - recommended_version = AwesomeVersion(RECOMMENDED_VERSION) - - # Assert that Docker version is equal to or greater than recommended version - assert docker_version >= recommended_version, ( - f"go2rtc Docker version ({docker_version}) is less than " - f"RECOMMENDED_VERSION ({recommended_version}). " - "Please update _GO2RTC_SHA in script/hassfest/docker.py to a newer version" - )