From a11727beb64086ce1bbef4e0150168d9b0e331f2 Mon Sep 17 00:00:00 2001 From: James Reilly Date: Thu, 10 Sep 2026 10:23:52 +0530 Subject: [PATCH] fix(renovate): match the freedesktop-sdk series the junction actually tracks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit renovate.json filters freedesktop-sdk tags with "extractVersion": "^freedesktop-sdk-(?25\\.08\\.[0-9]+)$" while elements/freedesktop-sdk.bst has tracked 26.08 since the bump to freedesktop-sdk-26.08.0-0-gdb97cce: track: freedesktop-sdk-26.08* ref: freedesktop-sdk-26.08.0-0-gdb97cce32cecadc7a3e98f06d557ebfa6ba9ad46 No tag upstream publishes can match that rule any more, so Renovate reports no updates rather than an error. The junction silently stops receiving bumps and the track-refs job in build.yml never has a ref to resolve. Nothing in any log says so — the failure looks exactly like "already up to date". Bumps the rule to 26.08, and adds check-renovate-series.py so the pair cannot drift apart again: it reads the series out of each junction's `track:` glob and asserts the matching extractVersion is anchored to the same one, failing closed. Same shape as check-release-version.py and check-k0s-version.py — wired into .pre-commit-config.yaml, and unit-tests.yml now also fires on renovate.json and elements/*.bst so the guard runs when either side moves. tests/unit/test_check_renovate_series.py covers the passing case, the exact regression above, a junction with no packageRule, a junction with no `track:`, the two series helpers, and the checked-in tree itself. $ python3 -m pytest tests/unit -q 207 passed, 1 xfailed in 0.64s Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VwEniffvKGH7gHfqKneuaJ --- .github/scripts/check-renovate-series.py | 125 +++++++++++++++++++++ .github/workflows/unit-tests.yml | 4 + .pre-commit-config.yaml | 6 ++ renovate.json | 2 +- tests/unit/test_check_renovate_series.py | 132 +++++++++++++++++++++++ 5 files changed, 268 insertions(+), 1 deletion(-) create mode 100755 .github/scripts/check-renovate-series.py create mode 100644 tests/unit/test_check_renovate_series.py diff --git a/.github/scripts/check-renovate-series.py b/.github/scripts/check-renovate-series.py new file mode 100755 index 0000000..016f501 --- /dev/null +++ b/.github/scripts/check-renovate-series.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Keep Renovate's version filters on the same release series as the junctions. + +`elements/*.bst` pin a junction with a `track:` glob and a resolved `ref:`: + + track: freedesktop-sdk-26.08* + ref: freedesktop-sdk-26.08.0-0-gdb97cce... + +`renovate.json` decides which upstream tags are candidates for a bump with a +per-package `extractVersion` anchored to that same series: + + "extractVersion": "^freedesktop-sdk-(?26\\.08\\.[0-9]+)$" + +Nothing ties the two together. When a junction moves to a new series and the +Renovate rule does not, the rule stops matching every tag upstream publishes. +Renovate then reports no updates rather than an error, so the junction quietly +stops receiving bumps and the `track-refs` job in build.yml has nothing to +resolve. That is not hypothetical: the freedesktop-sdk rule still read +`25\\.08\\.` after the junction moved to `freedesktop-sdk-26.08.0`. + +This script fails closed on that drift. It reads the series out of each +junction's `track:` glob and asserts the matching `extractVersion` in +renovate.json is anchored to the same one. +""" + +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +RENOVATE_JSON = ROOT / "renovate.json" + +# depName in renovate.json -> the junction whose `track:` is authoritative. +JUNCTIONS = { + "https://gitlab.com/freedesktop-sdk/freedesktop-sdk.git": ( + ROOT / "elements" / "freedesktop-sdk.bst" + ), + "https://gitlab.gnome.org/GNOME/gnome-build-meta.git": ( + ROOT / "elements" / "gnome-build-meta.bst" + ), +} + +TRACK_RE = re.compile(r"^\s*track:\s*[\"']?([^\"'\s]+)[\"']?\s*$", re.MULTILINE) + + +def read(path): + if not path.is_file(): + sys.exit(f"ERROR: expected file not found: {path.relative_to(ROOT)}") + return path.read_text(encoding="utf-8") + + +def series_of(track): + """The literal prefix of a track glob, with the trailing '*' removed. + + `freedesktop-sdk-26.08*` -> `freedesktop-sdk-26.08` + `gnome-50` -> `gnome-50` (no glob; the whole value) + """ + return track.split("*", 1)[0] + + +def digits(text): + """Version-ish digit groups, so an escaped regex and a plain glob compare. + + `^freedesktop-sdk-(?26\\.08\\.[0-9]+)$` -> ['26', '08'] + `freedesktop-sdk-26.08` -> ['26', '08'] + `[0-9]+` is dropped: it is the part Renovate fills in, not a series digit. + """ + stripped = text.replace("\\.", ".").replace("[0-9]+", "") + return [g for g in re.findall(r"\d+", stripped)] + + +def main(): + config = json.loads(read(RENOVATE_JSON)) + rules = config.get("packageRules", []) + failures = [] + checked = 0 + + for dep_name, junction_path in JUNCTIONS.items(): + matching = [ + rule + for rule in rules + if dep_name in rule.get("matchPackageNames", []) + and "extractVersion" in rule + ] + if not matching: + failures.append( + f"renovate.json has no packageRule with an extractVersion for " + f"{dep_name}; {junction_path.relative_to(ROOT)} would never be bumped" + ) + continue + + track_match = TRACK_RE.search(read(junction_path)) + if not track_match: + failures.append( + f"{junction_path.relative_to(ROOT)} declares no `track:`; " + "cannot verify the Renovate series against it" + ) + continue + + series = series_of(track_match.group(1)) + want = digits(series) + + for rule in matching: + checked += 1 + got = digits(rule["extractVersion"]) + if got != want: + failures.append( + f"{junction_path.relative_to(ROOT)} tracks `{track_match.group(1)}` " + f"(series {'.'.join(want) or ''}) but renovate.json filters " + f"{dep_name} with extractVersion {rule['extractVersion']!r} " + f"(series {'.'.join(got) or ''}). Renovate matches no tag, " + "so this junction gets no update PRs." + ) + + if failures: + for failure in failures: + print(f"ERROR: {failure}", file=sys.stderr) + sys.exit(1) + + print(f"OK: {checked} Renovate version filter(s) match their junction `track:` series") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index fc4485d..d94d599 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -8,6 +8,8 @@ on: - 'files/**' - 'tests/unit/**' - 'Justfile' + - 'renovate.json' + - 'elements/*.bst' push: branches: [main] paths: @@ -16,6 +18,8 @@ on: - 'files/**' - 'tests/unit/**' - 'Justfile' + - 'renovate.json' + - 'elements/*.bst' permissions: contents: read diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 95cf310..0a9f61a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,6 +18,12 @@ repos: language: system pass_filenames: false files: ^(project\.conf|elements/freedesktop-sdk\.bst|\.github/scripts/check-release-version\.py)$ + - id: check-renovate-series + name: Renovate version filters match the junction track series + entry: python .github/scripts/check-renovate-series.py + language: system + pass_filenames: false + files: ^(renovate\.json|elements/(freedesktop-sdk|gnome-build-meta)\.bst|\.github/scripts/check-renovate-series\.py)$ - id: check-k0s-version name: k0s version derived from include/k0s.yml, not restated entry: python .github/scripts/check-k0s-version.py diff --git a/renovate.json b/renovate.json index 45e9cdf..953e0c0 100644 --- a/renovate.json +++ b/renovate.json @@ -18,7 +18,7 @@ { "matchDatasources": ["git-refs"], "matchPackageNames": ["https://gitlab.com/freedesktop-sdk/freedesktop-sdk.git"], - "extractVersion": "^freedesktop-sdk-(?25\\.08\\.[0-9]+)$", + "extractVersion": "^freedesktop-sdk-(?26\\.08\\.[0-9]+)$", "versioning": "semver" }, { diff --git a/tests/unit/test_check_renovate_series.py b/tests/unit/test_check_renovate_series.py new file mode 100644 index 0000000..afda45e --- /dev/null +++ b/tests/unit/test_check_renovate_series.py @@ -0,0 +1,132 @@ +"""Unit coverage for .github/scripts/check-renovate-series.py. + +The script guards a silent failure: when a junction moves to a new release +series and renovate.json's extractVersion does not, Renovate matches no tag +and simply reports no updates. The junction then stops receiving bumps with +nothing in any log to say so. +""" + +import importlib.util +import json +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +SCRIPT = ROOT / ".github" / "scripts" / "check-renovate-series.py" + +# The script's filename is not a valid module name, so load it by path — +# same approach as tests/unit/test_check_k0s_version.py. +_spec = importlib.util.spec_from_file_location("check_renovate_series", SCRIPT) +check_renovate_series = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(check_renovate_series) + + +def run_against(tmp_path, renovate, junctions): + """Run the script with ROOT redirected at a throwaway tree.""" + (tmp_path / ".github" / "scripts").mkdir(parents=True) + (tmp_path / "elements").mkdir() + (tmp_path / "renovate.json").write_text(json.dumps(renovate)) + for name, body in junctions.items(): + (tmp_path / "elements" / name).write_text(body) + script_copy = tmp_path / ".github" / "scripts" / SCRIPT.name + script_copy.write_text(SCRIPT.read_text()) + return subprocess.run( + [sys.executable, str(script_copy)], capture_output=True, text=True + ) + + +FSDK = "https://gitlab.com/freedesktop-sdk/freedesktop-sdk.git" +GBM = "https://gitlab.gnome.org/GNOME/gnome-build-meta.git" + + +def rule(dep, extract): + return { + "matchDatasources": ["git-refs"], + "matchPackageNames": [dep], + "extractVersion": extract, + "versioning": "semver", + } + + +def junction(track): + return f"kind: junction\n\nsources:\n- kind: git_repo\n track: {track}\n ref: x\n" + + +BOTH_JUNCTIONS = { + "freedesktop-sdk.bst": junction("freedesktop-sdk-26.08*"), + "gnome-build-meta.bst": junction("gnome-50"), +} + + +def test_passes_when_series_agree(tmp_path): + result = run_against( + tmp_path, + { + "packageRules": [ + rule(FSDK, r"^freedesktop-sdk-(?26\.08\.[0-9]+)$"), + rule(GBM, r"^(?50\.[0-9]+)$"), + ] + }, + BOTH_JUNCTIONS, + ) + assert result.returncode == 0, result.stderr + assert "OK: 2" in result.stdout + + +def test_fails_on_the_real_regression(tmp_path): + """The shipped bug: junction on 26.08, Renovate still filtering 25.08.""" + result = run_against( + tmp_path, + { + "packageRules": [ + rule(FSDK, r"^freedesktop-sdk-(?25\.08\.[0-9]+)$"), + rule(GBM, r"^(?50\.[0-9]+)$"), + ] + }, + BOTH_JUNCTIONS, + ) + assert result.returncode == 1 + assert "matches no tag" in result.stderr + assert "freedesktop-sdk.bst" in result.stderr + + +def test_fails_when_a_junction_has_no_rule(tmp_path): + result = run_against( + tmp_path, + {"packageRules": [rule(GBM, r"^(?50\.[0-9]+)$")]}, + BOTH_JUNCTIONS, + ) + assert result.returncode == 1 + assert "no packageRule" in result.stderr + + +def test_fails_when_the_junction_declares_no_track(tmp_path): + result = run_against( + tmp_path, + { + "packageRules": [ + rule(FSDK, r"^freedesktop-sdk-(?26\.08\.[0-9]+)$"), + rule(GBM, r"^(?50\.[0-9]+)$"), + ] + }, + { + "freedesktop-sdk.bst": "kind: junction\n\nsources:\n- kind: git_repo\n ref: x\n", + "gnome-build-meta.bst": junction("gnome-50"), + }, + ) + assert result.returncode == 1 + assert "declares no `track:`" in result.stderr + + +def test_series_helpers_ignore_the_renovate_placeholder(): + assert check_renovate_series.series_of("freedesktop-sdk-26.08*") == "freedesktop-sdk-26.08" + assert check_renovate_series.series_of("gnome-50") == "gnome-50" + assert check_renovate_series.digits(r"^freedesktop-sdk-(?26\.08\.[0-9]+)$") == ["26", "08"] + assert check_renovate_series.digits("freedesktop-sdk-26.08") == ["26", "08"] + + +def test_the_real_repo_is_consistent(): + """Runs the script against the checked-in tree, so drift fails CI.""" + result = subprocess.run([sys.executable, str(SCRIPT)], capture_output=True, text=True) + assert result.returncode == 0, result.stderr