Skip to content

Commit affd70b

Browse files
authored
Merge pull request #614 from posit-dev/fix/dev-dep-channel-latest
fix: support `:{{ Channel }}` tags for matrix development versions
2 parents e6766a7 + 7e02652 commit affd70b

5 files changed

Lines changed: 114 additions & 0 deletions

File tree

posit-bakery/posit_bakery/config/image/dev_version/base.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,13 @@ def as_image_version(self):
275275

276276
version = self.get_version()
277277
metadata = {}
278+
# Dev versions that track a channel head expose resolved_channel_latest (set during
279+
# version resolution). Surface it so floating {{ Channel }} tags are suppressed when the
280+
# build targets an older-than-head version. getattr keeps this safe for any future
281+
# subclass that does not track a channel; absence defaults is_channel_latest to True.
282+
resolved_channel_latest = getattr(self, "resolved_channel_latest", None)
283+
if resolved_channel_latest is not None:
284+
metadata["channel_latest"] = resolved_channel_latest
278285
release_channel = self.get_release_channel()
279286
if release_channel is not None:
280287
metadata["release_channel"] = release_channel

posit-bakery/posit_bakery/config/image/dev_version/dependency.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ class ImageDevelopmentVersionFromDependency(BaseImageDevelopmentVersion):
3232
ReleaseChannelEnum | None,
3333
Field(default=None, description="Release channel for this dev version (e.g. 'daily', 'preview')."),
3434
] = None
35+
resolved_channel_latest: Annotated[
36+
bool,
37+
Field(
38+
exclude=True,
39+
default=False,
40+
description="Whether the resolved version equals the current channel head. "
41+
"Populated by _resolve_os_urls(); used to suppress floating {{ Channel }} tags "
42+
"for builds targeting older versions.",
43+
),
44+
]
3545

3646
@field_validator("channel", mode="after")
3747
@classmethod
@@ -44,14 +54,21 @@ def channel_not_release(cls, v: ReleaseChannelEnum | None) -> ReleaseChannelEnum
4454
return v
4555

4656
def get_version(self) -> str:
57+
# If overridden by a dev spec, return immediately and do not assume latest.
4758
if self.version_override is not None:
4859
return self.version_override
60+
61+
# If resolving a version, assume latest.
62+
self.resolved_channel_latest = True
63+
64+
# Resolve the latest version via constraint with prerelease.
4965
constraint_class = get_dependency_constraint_class(self.dependency)
5066
constraint = constraint_class(
5167
prerelease=self.prerelease,
5268
constraint=VersionConstraint(latest=True, count=1),
5369
)
5470
result = constraint.resolve_versions()
71+
5572
return str(result.versions[0])
5673

5774
def _get_dependencies_for_matrix(self, version: str) -> list:

posit-bakery/posit_bakery/config/tag.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,5 +194,17 @@ def default_matrix_tag_patterns() -> list[TagPattern]:
194194
patterns=["{{ Version | stripPatch }}"],
195195
only=[TagPatternFilter.LATEST_PATCH, TagPatternFilter.PRIMARY_OS, TagPatternFilter.PRIMARY_VARIANT],
196196
),
197+
TagPattern(
198+
patterns=["{{ Channel }}-{{ Variant }}"],
199+
only=[TagPatternFilter.CHANNEL_LATEST, TagPatternFilter.PRIMARY_OS],
200+
),
201+
TagPattern(
202+
patterns=["{{ Channel }}-{{ OS }}"],
203+
only=[TagPatternFilter.CHANNEL_LATEST, TagPatternFilter.PRIMARY_VARIANT],
204+
),
205+
TagPattern(
206+
patterns=["{{ Channel }}"],
207+
only=[TagPatternFilter.CHANNEL_LATEST, TagPatternFilter.PRIMARY_OS, TagPatternFilter.PRIMARY_VARIANT],
208+
),
197209
*_shared_latest_tag_patterns(),
198210
]

posit-bakery/test/config/image/dev_version/test_dependency.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,36 @@ def test_release_channel_in_metadata_when_channel_set(self, patch_requests_get):
317317
iv = dev.as_image_version()
318318
assert iv.metadata["release_channel"] == "daily"
319319

320+
def test_channel_latest_true_in_metadata_when_resolving_head(self, patch_requests_get):
321+
"""Resolving the latest version marks channel_latest True so floating Channel tags emit."""
322+
dev = ImageDevelopmentVersionFromDependency(
323+
parent=_mock_parent(),
324+
dependency="positron",
325+
prerelease=True,
326+
channel="daily",
327+
os=[_UBUNTU_24_OS],
328+
)
329+
iv = dev.as_image_version()
330+
assert iv.metadata["channel_latest"] is True
331+
332+
def test_channel_latest_false_in_metadata_with_version_override(self):
333+
"""A dev build pinned to an older version must mark channel_latest False so the floating
334+
Channel tag is suppressed (it should keep pointing at the head, not the pinned build).
335+
336+
Regression test: resolved_channel_latest was previously never surfaced into metadata, so
337+
is_channel_latest defaulted to True and the floating Channel tag emitted for older pins.
338+
"""
339+
dev = ImageDevelopmentVersionFromDependency(
340+
parent=_mock_parent(),
341+
dependency="positron",
342+
channel="daily",
343+
os=[_UBUNTU_24_OS],
344+
)
345+
# version_override short-circuits resolution, so no network call is made.
346+
dev.version_override = "2020.01.0-1"
347+
iv = dev.as_image_version()
348+
assert iv.metadata["channel_latest"] is False
349+
320350
def test_os_preserved(self, patch_requests_get):
321351
dev = ImageDevelopmentVersionFromDependency(
322352
parent=_mock_parent(),

posit-bakery/test/config/test_tag.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,54 @@ def test_default_matrix_tag_patterns_includes_strip_patch_under_latest_patch_fil
158158
)
159159

160160

161+
def test_default_matrix_tag_patterns_includes_channel_patterns():
162+
"""Matrix tag patterns include floating {{ Channel }} patterns gated by CHANNEL_LATEST.
163+
164+
These give matrix dev images channel-head tags (e.g. "daily", "daily-min") that the
165+
CHANNEL_LATEST filter suppresses for builds targeting an older-than-head version.
166+
"""
167+
patterns = default_matrix_tag_patterns()
168+
channel_patterns = [p for p in patterns if any("{{ Channel }}" in pat for pat in p.patterns)]
169+
170+
# The composite "{{ Version }}" axis is dropped for channel tags, so only the primary-reduced
171+
# variants are emitted to avoid collisions across matrix combinations.
172+
expected = {
173+
(("{{ Channel }}-{{ Variant }}",), (TagPatternFilter.CHANNEL_LATEST, TagPatternFilter.PRIMARY_OS)),
174+
(("{{ Channel }}-{{ OS }}",), (TagPatternFilter.CHANNEL_LATEST, TagPatternFilter.PRIMARY_VARIANT)),
175+
(
176+
("{{ Channel }}",),
177+
(TagPatternFilter.CHANNEL_LATEST, TagPatternFilter.PRIMARY_OS, TagPatternFilter.PRIMARY_VARIANT),
178+
),
179+
}
180+
assert {(tuple(p.patterns), tuple(p.only)) for p in channel_patterns} == expected
181+
182+
# Every channel pattern must be gated by CHANNEL_LATEST so older-than-head builds suppress them.
183+
for pattern in channel_patterns:
184+
assert TagPatternFilter.CHANNEL_LATEST in pattern.only, (
185+
f"Channel pattern must be gated by CHANNEL_LATEST: {pattern.patterns} only={pattern.only}"
186+
)
187+
188+
189+
def test_default_matrix_channel_patterns_render_with_channel():
190+
"""Channel patterns render floating channel tags when a Channel value is supplied."""
191+
patterns = [p for p in default_matrix_tag_patterns() if any("{{ Channel }}" in pat for pat in p.patterns)]
192+
rendered = []
193+
for pattern in patterns:
194+
rendered.extend(pattern.render(Channel="daily", OS="ubuntu2404", Variant="min"))
195+
196+
assert set(rendered) == {"daily-min", "daily-ubuntu2404", "daily"}
197+
198+
199+
def test_default_matrix_channel_patterns_render_nothing_without_channel():
200+
"""Channel patterns produce no tags when no Channel value is supplied (non-channel images)."""
201+
patterns = [p for p in default_matrix_tag_patterns() if any("{{ Channel }}" in pat for pat in p.patterns)]
202+
rendered = []
203+
for pattern in patterns:
204+
rendered.extend(pattern.render(Version="R4.3.3-python3.11.15", OS="ubuntu2404", Variant="min"))
205+
206+
assert rendered == []
207+
208+
161209
def test_default_matrix_tag_patterns_strip_patch_renders_minor_tags():
162210
"""stripPatch-filtered tag patterns produce minor-only tags from composite matrix versions."""
163211
patterns = [p for p in default_matrix_tag_patterns() if TagPatternFilter.LATEST_PATCH in p.only]

0 commit comments

Comments
 (0)