Skip to content

Commit 15a476e

Browse files
committed
linters: add check_sru_version_string_convention
Add a check for expected version string for SRUs, using the conventions documented in the project docs. There is room for future improvement, most notably for native packages, which are not supported by the check at all right now. This is just a time restriction at the present moment, and should be implemented in the future. This is off for devel and a failure for stable by default.
1 parent 72ae8c1 commit 15a476e

File tree

4 files changed

+241
-0
lines changed

4 files changed

+241
-0
lines changed

tests/test_linters.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,152 @@ def test_check_sru_version_string_breaks_upgrades(requests_mock):
250250
ubuntu_lint.check_sru_version_string_breaks_upgrades(
251251
ubuntu_lint.Context(changes=changes_bad_version)
252252
)
253+
254+
255+
def test_check_sru_version_string_convention(requests_mock):
256+
changelog_tmpl = """hello ({next_version}) noble; urgency=medium
257+
258+
* Fix a bug (LP: #12345678)
259+
260+
-- John Doe <john.doe@example.com> Wed, 11 Mar 2026 16:01:41 -0400
261+
262+
hello ({prev_version}) noble; urgency=high
263+
264+
* No change rebuild for 64-bit time_t and frame pointers.
265+
266+
-- John Doe <john.doe@example.com> Mon, 08 Apr 2024 17:58:52 +0200
267+
"""
268+
269+
rmadison_tmpls = [
270+
textwrap.dedent(
271+
"""hello | 2.8-4 | trusty | source
272+
hello | 2.10-1 | xenial | source
273+
hello | 2.10-1build1 | bionic | source
274+
hello | 2.10-1build3 | bionic-security | source
275+
hello | 2.10-1build3 | bionic-updates | source
276+
hello | 2.10-2ubuntu2 | focal | source
277+
hello | 2.10-2ubuntu4 | jammy | source
278+
hello | {prev_version}| noble | source
279+
hello | 2.10-5 | questing | source
280+
hello | 2.10-5build1 | resolute | source
281+
"""),
282+
# Same version in two releases
283+
textwrap.dedent(
284+
"""hello | 2.8-4 | trusty | source
285+
hello | 2.10-1 | xenial | source
286+
hello | 2.10-1build1 | bionic | source
287+
hello | 2.10-1build3 | bionic-security | source
288+
hello | 2.10-1build3 | bionic-updates | source
289+
hello | 2.10-2ubuntu2 | focal | source
290+
hello | 2.10-2ubuntu4 | jammy | source
291+
hello | {prev_version}| noble | source
292+
hello | {prev_version}| questing | source
293+
hello | 2.10-5build1 | resolute | source
294+
"""),
295+
]
296+
297+
# (prev_version, next_version, expect_pass)
298+
testcases_list = [
299+
[
300+
# 2.10-3 -> "2.10-3ubuntu0.1"
301+
("2.10-3", "2.10-3ubuntu0.1", True),
302+
("2.10-3", "2.10-3ubuntu1", False),
303+
("2.10-3", "2.10-4", False),
304+
# 2.10-3ubuntu0.1 -> "2.10-3ubuntu0.2"
305+
("2.10-3ubuntu0.1", "2.10-3ubuntu0.2", True),
306+
("2.10-3ubuntu0.1", "2.10-3ubuntu1", False),
307+
# 2.10-3ubuntu2 -> "2.10-3ubuntu2.1"
308+
("2.10-3ubuntu2", "2.10-3ubuntu2.1", True),
309+
("2.10-3ubuntu2", "2.10-3ubuntu3", False),
310+
# 2.10-3ubuntu2.1 -> "2.10-3ubuntu2.2"
311+
("2.10-3ubuntu2.1", "2.10-3ubuntu2.2", True),
312+
("2.10-3ubuntu2.1", "2.10-3ubuntu3", False),
313+
# 2.10-3build1 -> 2.10-3ubuntu0.1
314+
("2.10-3build1", "2.10-3ubuntu0.1", True),
315+
("2.10-3build1", "2.10-3build2", False),
316+
("2.10-3build1", "2.10-3ubuntu1", False),
317+
# 2.10-3ubuntu0.24.04.1 -> 2.10-3ubuntu0.24.04.2
318+
("2.10-3ubuntu0.24.04.1", "2.10-3ubuntu0.24.04.2", True),
319+
("2.10-3ubuntu0.24.04.1", "2.10-3ubuntu1", False),
320+
],
321+
[
322+
# 2.10-5 in two releases -> 2.10-5ubuntu0.24.04.1
323+
("2.10-5", "2.10-5ubuntu0.24.04.1", True),
324+
("2.10-5", "2.10-5ubuntu0.1", False),
325+
("2.10-5", "2.10-5ubuntu1", False),
326+
("2.10-5", "2.10-6", False),
327+
# 2.10-5build1 in two releases -> 2.10-5ubuntu0.24.04.1
328+
("2.10-5build1", "2.10-5ubuntu0.24.04.1", True),
329+
("2.10-5build1", "2.10-5ubuntu0.1", False),
330+
("2.10-5build1", "2.10-5ubuntu1", False),
331+
("2.10-5build1", "2.10-5build2", False),
332+
# 2.10-5ubuntu1 in two releases -> 2.10-5ubuntu1.24.04.1
333+
("2.10-5ubuntu1", "2.10-5ubuntu1.24.04.1", True),
334+
("2.10-5ubuntu1", "2.10-5ubuntu1.1", False),
335+
("2.10-5ubuntu1", "2.10-5ubuntu2", False),
336+
# 2.10-5ubuntu1.1 in two releases -> 2.10-5ubuntu1.1.24.04.1
337+
("2.10-5ubuntu1.1", "2.10-5ubuntu1.1.24.04.1", True),
338+
("2.10-5ubuntu1.1", "2.10-5ubuntu1.2", False),
339+
("2.10-5ubuntu1.1", "2.10-5ubuntu2", False),
340+
],
341+
]
342+
343+
for i in range(len(testcases_list)):
344+
rmadison_tmpl = rmadison_tmpls[i]
345+
testcases = testcases_list[i]
346+
347+
for (prev_version, next_version, expect_pass) in testcases:
348+
requests_mock.get(
349+
"https://people.canonical.com/~ubuntu-archive/madison.cgi?package=hello&a=source&text=on",
350+
text=rmadison_tmpl.format(prev_version=prev_version)
351+
)
352+
debian_changelog = changelog.Changelog(
353+
changelog_tmpl.format(
354+
prev_version=prev_version,
355+
next_version=next_version,
356+
)
357+
)
358+
context = ubuntu_lint.Context(debian_changelog=debian_changelog)
359+
360+
if expect_pass:
361+
ubuntu_lint.check_sru_version_string_convention(context)
362+
else:
363+
with pytest.raises(
364+
ubuntu_lint.LintFailure,
365+
match=f"{next_version} does not match expected version"
366+
):
367+
ubuntu_lint.check_sru_version_string_convention(context)
368+
369+
# Test cases for new upstream version
370+
testcases = [
371+
# 2.10-3ubuntu1 -> 2.11-0ubuntu0.24.04.1 (new upstream)
372+
("2.10-3ubuntu1", "2.11-0ubuntu0.24.04.1", True),
373+
("2.10-3ubuntu1", "2.11-0ubuntu0.1", False),
374+
# 2.10-3ubuntu1 -> 2.11-0ubuntu0~24.04.1 (new upstream)
375+
("2.10-3ubuntu1", "2.11-0ubuntu0~24.04.1", True),
376+
# 2.10-3ubuntu1 -> 2.11-1ubuntu2~24.04.1 (devel backport)
377+
("2.10-3ubuntu1", "2.11-1ubuntu2~24.04.1", True),
378+
("2.10-3ubuntu1", "2.11-1ubuntu2", False),
379+
]
380+
381+
for (prev_version, next_version, expect_pass) in testcases:
382+
requests_mock.get(
383+
"https://people.canonical.com/~ubuntu-archive/madison.cgi?package=hello&a=source&text=on",
384+
text=rmadison_tmpls[0].format(prev_version=prev_version)
385+
)
386+
debian_changelog = changelog.Changelog(
387+
changelog_tmpl.format(
388+
prev_version=prev_version,
389+
next_version=next_version,
390+
)
391+
)
392+
context = ubuntu_lint.Context(debian_changelog=debian_changelog)
393+
394+
if expect_pass:
395+
ubuntu_lint.check_sru_version_string_convention(context)
396+
else:
397+
with pytest.raises(
398+
ubuntu_lint.LintFailure,
399+
match="version string for new upstream should contain suffix"
400+
):
401+
ubuntu_lint.check_sru_version_string_convention(context)

ubuntu_lint/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
check_sru_bug_missing_template,
1717
check_sru_bug_missing_release_tasks,
1818
check_sru_version_string_breaks_upgrades,
19+
check_sru_version_string_convention,
1920
)
2021

2122
__all__ = [
@@ -31,4 +32,5 @@
3132
"check_sru_bug_missing_template",
3233
"check_sru_bug_missing_release_tasks",
3334
"check_sru_version_string_breaks_upgrades",
35+
"check_sru_version_string_convention",
3436
]

ubuntu_lint/cli.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class Runner:
1919
"sru-bug-missing-template": ubuntu_lint.check_sru_bug_missing_template,
2020
"sru-bug-missing-release-tasks": ubuntu_lint.check_sru_bug_missing_release_tasks,
2121
"sru-version-string-breaks-upgrades": ubuntu_lint.check_sru_version_string_breaks_upgrades,
22+
"sru-version-string-convention": ubuntu_lint.check_sru_version_string_convention,
2223
}
2324

2425
# Default actions when linting development release.
@@ -32,6 +33,7 @@ class Runner:
3233
"sru-bug-missing-template": "off",
3334
"sru-bug-missing-release-tasks": "off",
3435
"sru-version-string-breaks-upgrades": "off",
36+
"sru-version-string-convention": "off",
3537
}
3638

3739
# Default actions when linting stable releases.
@@ -45,6 +47,7 @@ class Runner:
4547
"sru-bug-missing-template": "warn",
4648
"sru-bug-missing-release-tasks": "warn",
4749
"sru-version-string-breaks-upgrades": "warn",
50+
"sru-version-string-convention": "warn",
4851
}
4952

5053
def __init__(self):

ubuntu_lint/linters.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,3 +275,90 @@ def check_sru_version_string_breaks_upgrades(context: Context):
275275
f"{target_version} for {target_series} is greater than {v} for {s}, "
276276
"which breaks the upgrade path"
277277
)
278+
279+
280+
def check_sru_version_string_convention(context: Context):
281+
"""
282+
Examines the package version string to determine if it is appropriate for SRU.
283+
"""
284+
docs = "https://documentation.ubuntu.com/project/how-ubuntu-is-made/concepts/version-strings"
285+
286+
next_version = context.changelog_entry.version
287+
prev_version = context.changelog_entry_by_index(1).version
288+
289+
try:
290+
if next_version.full_version != context.changes.get("Version"):
291+
context.lint_fail("changes file version does not match changelog version")
292+
except MissingContextException:
293+
pass
294+
295+
match = re.search(r"-[0-9]*", prev_version.full_version)
296+
if match:
297+
(upstream_version, debian_revison, ubuntu_revision) = prev_version.full_version.partition(match.group())
298+
else:
299+
context.lint_fail(
300+
"check not implemented for native packages, "
301+
f"please check {docs} to ensure version string is correct"
302+
)
303+
304+
dists = context.changelog_entry.distributions
305+
if dists is not None:
306+
series_version = distro_info.UbuntuDistroInfo().version(dists.partition('-')[0])
307+
# Strip off " LTS" if needed.
308+
series_version = series_version.partition(' ')[0]
309+
else:
310+
context.lint_fail("changelog entry is missing distribution field")
311+
312+
if debian_support.version_compare(next_version.upstream_version, prev_version.upstream_version) > 0:
313+
# Handle new upstream version separately from the rest. This check could be
314+
# expanded in the future to cover more cases, but at the very least, the new
315+
# version should end in e.g. 24.04.1.
316+
if not next_version.full_version.endswith(f"{series_version}.1"):
317+
context.lint_fail(
318+
f"version string for new upstream should contain suffix {series_version}.1"
319+
)
320+
return
321+
322+
# If the previous version is published across multiple series, then we expect e.g.
323+
# ubuntu24.04.x suffixes.
324+
max_version_by_series = _rmadision_get_max_version_by_series(context)
325+
series_with_version = list(max_version_by_series.values()).count(prev_version)
326+
327+
if series_with_version < 1:
328+
context.lint_fail("rmadison output is not consistent with changelog")
329+
330+
suffix_extra: str = ""
331+
if series_with_version > 1:
332+
suffix_extra = f".{series_version}"
333+
334+
expect: str = ""
335+
if not ubuntu_revision:
336+
# E.g. 2.0-1 -> 2.0-1ubuntu0.1
337+
expect = f"{upstream_version}{debian_revison}ubuntu0{suffix_extra}.1"
338+
elif "ubuntu" not in ubuntu_revision:
339+
# E.g. 2.0-1build1 -> 2.0-1ubuntu0.1
340+
expect = f"{upstream_version}{debian_revison}ubuntu0{suffix_extra}.1"
341+
elif "." not in ubuntu_revision:
342+
# E.g. 2.0-1ubuntu1 -> 2.0-1ubuntu1.1
343+
expect = f"{str(prev_version)}{suffix_extra}.1"
344+
elif suffix_extra:
345+
# All other cases where there multiple series with the same version.
346+
expect = f"{str(prev_version)}{suffix_extra}.1"
347+
else:
348+
# E.g. 2.0-1ubuntu1.1 -> 2.0-1ubuntu1.2
349+
try:
350+
parts = ubuntu_revision.split(".")
351+
parts[-1] = str(int(parts[-1]) + 1)
352+
new_ubuntu_revision = ".".join(parts)
353+
354+
expect = f"{upstream_version}{debian_revison}{new_ubuntu_revision}"
355+
except ValueError:
356+
context.lint_fail(
357+
f"cannot handle version string format {prev_version}"
358+
)
359+
360+
if next_version.full_version != expect:
361+
context.lint_fail(
362+
f"{next_version} does not match expected version {expect}, "
363+
f"see {docs} for expected version string conventions"
364+
)

0 commit comments

Comments
 (0)