From 8becd89ff2e7067b112b03c0276cd9ad505c4554 Mon Sep 17 00:00:00 2001 From: Frank Niessink Date: Fri, 13 Feb 2026 16:31:16 +0100 Subject: [PATCH] Improve just update-deps - Improve names of scripts to update deps. - Update Docker images used in CircleCI config.yml. --- justfile | 13 ++- tools/update_circle_ci_config.py | 88 +++++++++++++++++++ ...ate.py => update_dockerfile_base_image.py} | 0 ...tion_update.py => update_github_action.py} | 0 ...{uv_update.py => update_pyproject_toml.py} | 0 5 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 tools/update_circle_ci_config.py rename tools/{dockerfile_base_image_update.py => update_dockerfile_base_image.py} (100%) rename tools/{github_action_update.py => update_github_action.py} (100%) rename tools/{uv_update.py => update_pyproject_toml.py} (100%) diff --git a/justfile b/justfile index a2f1c9fffa..b07299d6f8 100644 --- a/justfile +++ b/justfile @@ -7,7 +7,7 @@ _default: alias help := _default export COVERAGE_RCFILE := justfile_directory() + "/.coveragerc" -uv_update_script := justfile_directory() + "/tools/uv_update.py" +update_pyproject_toml_script := justfile_directory() + "/tools/update_pyproject_toml.py" docker_folder_exists := path_exists(invocation_directory() + '/docker') src_folder_exists := path_exists(invocation_directory() + '/src') tests_folder_exists := path_exists(invocation_directory() + '/tests') @@ -31,20 +31,25 @@ random_string := uuid() # === Update dependencies === +# Update Docker images in the CircleCI config. +[private] +update-circle-ci-config: + uv run --script tools/update_circle_ci_config.py + # Update Docker base images in Dockerfiles. [private] update-docker-base-images: - uv run --script tools/dockerfile_base_image_update.py + uv run --script tools/update_dockerfile_base_image.py # Update GitHub Actions in GitHub workflow YAML files. [private] update-github-actions: - uv run --script tools/github_action_update.py + uv run --script tools/update_github_action.py # Update direct and indirect Python dependencies. [private] update-py-dependencies folder: - uv run --frozen --no-sync --directory "{{ folder }}" --script "{{ uv_update_script }}" + uv run --frozen --no-sync --directory "{{ folder }}" --script "{{ update_pyproject_toml_script }}" uv sync --upgrade --quiet --no-progress --directory "{{ folder }}" # Update direct and indirect JavaScript dependencies. diff --git a/tools/update_circle_ci_config.py b/tools/update_circle_ci_config.py new file mode 100644 index 0000000000..0c5d70b6f3 --- /dev/null +++ b/tools/update_circle_ci_config.py @@ -0,0 +1,88 @@ +# /// script +# requires-python = ">=3.14" +# dependencies = [ +# "packaging>=26.0", +# "requests>=2.32.5", +# ] +# /// + +"""CircleCI config updater script finds images and updates to the latest versions.""" + +import re +import sys +from functools import cache +from pathlib import Path + +import requests +from packaging.version import InvalidVersion, Version + + +@cache +def get_available_tags(image: str) -> list[str]: + """Fetch available tags for a Docker image from Docker Hub.""" + namespace, repository = image.split("/", maxsplit=1) if "/" in image else ("library", image) + url = f"https://registry.hub.docker.com/v2/namespaces/{namespace}/repositories/{repository}/tags?page_size=100" + tags = [] + while url: + response = requests.get(url, timeout=10) + response.raise_for_status() + json = response.json() + tags.extend([result["name"] for result in json.get("results", [])]) + url = json.get("next") + return tags + + +def split_tag(tag: str) -> tuple[Version | None, str]: + """Split the tag in a version and suffix. If the tag doesn't contain a valid version, the version is None.""" + version_string, suffix = tag.split("-", maxsplit=1) if "-" in tag else (tag, "") + try: + version = Version(version_string) + except InvalidVersion: + version = None + return version, suffix + + +def assemble_tag(version: Version, suffix: str) -> str: + """Assemble a tag from the version and suffix.""" + return f"{version}-{suffix}" if suffix else str(version) + + +def get_latest_tag(image: str, current_tag: str) -> str: + """Find the latest compatible tag for an image. Keeps the same non-numerical parts while upgrading the version.""" + current_version, current_suffix = split_tag(current_tag) + if current_version is None: + return current_tag # Can't determine a newer tag if the tag doesn't contain a valid version + latest_version = current_version + for tag in get_available_tags(image): + version, suffix = split_tag(tag) + if version is None: + continue # Ignore tags if the version is not valid + if version.is_prerelease: + continue # Ignore tags if the version is a prereleease + if suffix != current_suffix: + continue # Ignore tags with a different suffix because we don't want to change e.g. fat to slim + latest_version = max(latest_version, version) + return assemble_tag(latest_version, current_suffix) + + +def update_config_yml_line(line: str) -> str: + """Update the config YAML if the line contains an image statement, otherwise return the line unchanged.""" + if match := re.search(r"image: (?P[\w\d\./-]+):(?P[\d\w\.\-]+)", line): + image = match.group("image") + tag = match.group("tag") + return line.replace(tag, get_latest_tag(image, tag)) + return line + + +def update_circle_ci_config() -> int: + """Update images in the Circle CI config.""" + config_yml = Path.cwd() / ".circleci" / "config.yml" + old_lines = config_yml.read_text().splitlines() + new_lines = [update_config_yml_line(line) for line in old_lines] + if old_lines != new_lines: + config_yml.write_text("\n".join(new_lines) + "\n") + return 0 + + +if __name__ == "__main__": + sys.exit(update_circle_ci_config()) diff --git a/tools/dockerfile_base_image_update.py b/tools/update_dockerfile_base_image.py similarity index 100% rename from tools/dockerfile_base_image_update.py rename to tools/update_dockerfile_base_image.py diff --git a/tools/github_action_update.py b/tools/update_github_action.py similarity index 100% rename from tools/github_action_update.py rename to tools/update_github_action.py diff --git a/tools/uv_update.py b/tools/update_pyproject_toml.py similarity index 100% rename from tools/uv_update.py rename to tools/update_pyproject_toml.py