Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions .github/scripts/check-renovate-series.py
Original file line number Diff line number Diff line change
@@ -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-(?<version>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-(?<version>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 '<none>'}) but renovate.json filters "
f"{dep_name} with extractVersion {rule['extractVersion']!r} "
f"(series {'.'.join(got) or '<none>'}). 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()
4 changes: 4 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ on:
- 'files/**'
- 'tests/unit/**'
- 'Justfile'
- 'renovate.json'
- 'elements/*.bst'
push:
branches: [main]
paths:
Expand All @@ -16,6 +18,8 @@ on:
- 'files/**'
- 'tests/unit/**'
- 'Justfile'
- 'renovate.json'
- 'elements/*.bst'

permissions:
contents: read
Expand Down
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
{
"matchDatasources": ["git-refs"],
"matchPackageNames": ["https://gitlab.com/freedesktop-sdk/freedesktop-sdk.git"],
"extractVersion": "^freedesktop-sdk-(?<version>25\\.08\\.[0-9]+)$",
"extractVersion": "^freedesktop-sdk-(?<version>26\\.08\\.[0-9]+)$",
"versioning": "semver"
},
{
Expand Down
132 changes: 132 additions & 0 deletions tests/unit/test_check_renovate_series.py
Original file line number Diff line number Diff line change
@@ -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-(?<version>26\.08\.[0-9]+)$"),
rule(GBM, r"^(?<version>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-(?<version>25\.08\.[0-9]+)$"),
rule(GBM, r"^(?<version>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"^(?<version>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-(?<version>26\.08\.[0-9]+)$"),
rule(GBM, r"^(?<version>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-(?<version>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
Loading