Compare commits

...

2 Commits

Author SHA1 Message Date
Robert Resch 0f08be933a Merge branch 'dev' into go2rtc_fix_annoying_test 2026-05-18 21:45:23 +00:00
Erik a68ad41469 Don't spin up docker container when validating go2rtc version 2026-05-18 22:04:37 +02:00
4 changed files with 112 additions and 77 deletions
+2 -2
View File
@@ -6,6 +6,6 @@ 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.
# When changing this version, also update _GO2RTC_VERSION and the corresponding
# SHA hash (_GO2RTC_SHA) in script/hassfest/docker.py.
RECOMMENDED_VERSION = "1.9.14"
+86 -2
View File
@@ -1,7 +1,9 @@
"""Generate and validate the dockerfile."""
from dataclasses import dataclass
import json
from pathlib import Path
import urllib.request
from homeassistant import core
from homeassistant.util import executor, thread
@@ -13,10 +15,86 @@ _DOCKERFILE_SYNTAX_SHA = (
"2780b5c3bab67f1f76c781860de469442999ed1a0d7992a5efdf2cffc0e3d769" # 1.23.0
)
_GO2RTC_SHA = (
"675c318b23c06fd862a61d262240c9a63436b4050d177ffc68a32710d9e05bae" # 1.9.14
# go2rtc image pinned by digest. _GO2RTC_VERSION is the version that digest
# resolves to and MUST be kept in sync with _GO2RTC_SHA: both are updated
# together when bumping go2rtc. When either changes, hassfest verifies against
# the GHCR registry that the digest really is the one published for that
# version and records the verified pair in script/hassfest/generated/go2rtc.json
# so the check is not repeated on every CI run.
_GO2RTC_IMAGE = "alexxit/go2rtc"
_GO2RTC_VERSION = "1.9.14"
_GO2RTC_SHA = "675c318b23c06fd862a61d262240c9a63436b4050d177ffc68a32710d9e05bae"
_GO2RTC_RECORD_PATH = "script/hassfest/generated/go2rtc.json"
_MANIFEST_ACCEPT = (
"application/vnd.oci.image.index.v1+json,"
"application/vnd.docker.distribution.manifest.list.v2+json,"
"application/vnd.docker.distribution.manifest.v2+json"
)
def _go2rtc_record_content() -> str:
"""Return the content of the generated go2rtc SHA->version record."""
return (
json.dumps(
{"version": _GO2RTC_VERSION, "sha": _GO2RTC_SHA},
indent=2,
)
+ "\n"
)
def _registry_digest_for_version(version: str) -> str:
"""Resolve a go2rtc version tag to its image digest via the GHCR registry."""
with urllib.request.urlopen(
f"https://ghcr.io/token?scope=repository:{_GO2RTC_IMAGE}:pull&service=ghcr.io",
timeout=30,
) as response:
token = json.load(response)["token"]
request = urllib.request.Request(
f"https://ghcr.io/v2/{_GO2RTC_IMAGE}/manifests/{version}",
method="HEAD",
headers={
"Authorization": f"Bearer {token}",
"Accept": _MANIFEST_ACCEPT,
},
)
with urllib.request.urlopen(request, timeout=30) as response:
return response.headers.get("Docker-Content-Digest", "")
def _verify_go2rtc_record(config: Config) -> None:
"""Verify the pinned go2rtc digest matches the pinned version.
The (relatively expensive, network-bound) registry lookup only runs when
the pinned SHA/version differs from the previously verified pair recorded
in the generated file, i.e. when go2rtc is being bumped.
"""
record_path = config.root / _GO2RTC_RECORD_PATH
if record_path.is_file() and record_path.read_text() == _go2rtc_record_content():
return
try:
digest = _registry_digest_for_version(_GO2RTC_VERSION)
except OSError as err:
config.add_error(
"docker",
f"Unable to verify go2rtc {_GO2RTC_VERSION} against the registry: {err}",
)
return
expected = f"sha256:{_GO2RTC_SHA}"
if digest != expected:
actual = digest or "an unknown digest"
config.add_error(
"docker",
f"go2rtc {_GO2RTC_VERSION} resolves to {actual}, not {expected}. "
"Update _GO2RTC_VERSION and _GO2RTC_SHA in script/hassfest/docker.py "
"so they refer to the same release.",
)
DOCKERFILE_TEMPLATE = r"""# syntax=docker/dockerfile@sha256:{dockerfile_syntax}
# Automatically generated by hassfest.
#
@@ -215,6 +293,10 @@ def _generate_files(config: Config) -> list[File]:
),
config.root / "script/hassfest/docker/Dockerfile",
),
File(
_go2rtc_record_content(),
config.root / _GO2RTC_RECORD_PATH,
),
]
for machine_name, machine_config in sorted(_MACHINES.items()):
@@ -230,6 +312,8 @@ def _generate_files(config: Config) -> list[File]:
def validate(integrations: dict[str, Integration], config: Config) -> None:
"""Validate dockerfile."""
_verify_go2rtc_record(config)
docker_files = _generate_files(config)
config.cache["docker"] = docker_files
+4
View File
@@ -0,0 +1,4 @@
{
"version": "1.9.14",
"sha": "675c318b23c06fd862a61d262240c9a63436b4050d177ffc68a32710d9e05bae"
}
+20 -73
View File
@@ -1,86 +1,33 @@
"""Test that the go2rtc Docker image version matches or exceeds the recommended version.
"""Test that the pinned go2rtc Docker image 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 go2rtc image does not expose its version as an OCI label (the version is
only compiled into the binary). The SHA->version mapping is therefore recorded
in script/hassfest/docker.py as the ``_GO2RTC_VERSION`` constant paired with
``_GO2RTC_SHA``. hassfest verifies, against the GHCR registry, that the digest
really is the one published for that version whenever the pair changes, and
stores the verified pair in script/hassfest/generated/go2rtc.json so the check
is not repeated on every CI run.
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.
Given that verified mapping, this test only needs to assert, offline, that the
pinned version is equal to or greater than the RECOMMENDED_VERSION defined in
homeassistant/components/go2rtc/const.py, catching a RECOMMENDED_VERSION bump
without a corresponding image bump (or vice versa).
"""
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
from script.hassfest.docker import _GO2RTC_VERSION as DOCKER_VERSION
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)
def test_docker_version_matches_recommended() -> None:
"""Test that the pinned go2rtc Docker version is >= RECOMMENDED_VERSION."""
docker_version = AwesomeVersion(DOCKER_VERSION)
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"
f"The pinned go2rtc Docker version ({docker_version}) is less than "
f"RECOMMENDED_VERSION ({recommended_version}). Please update "
"_GO2RTC_VERSION and _GO2RTC_SHA in script/hassfest/docker.py to a "
"newer version"
)