Skip to content

Commit 7e02652

Browse files
ianpittwoodclaude
andcommitted
fix: surface resolved_channel_latest into dev version metadata
ImageDevelopmentVersionFromDependency set resolved_channel_latest during version resolution but relied on the base as_image_version(), which never copied it into ImageVersion.metadata. Only the channel subclass wrote metadata["channel_latest"], so dependency dev builds pinned to an older version (via version_override) still emitted the floating {{ Channel }} tag, pointing it at the wrong build. Surface resolved_channel_latest into metadata from the base as_image_version() so the CHANNEL_LATEST tag filter suppresses floating channel tags for older-than-head dependency builds. getattr keeps this safe for any future subclass that does not track a channel. Add regression tests for the dependency channel_latest metadata (head vs version_override) and cover the previously-untested matrix {{ Channel }} tag patterns. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent edd551e commit 7e02652

3 files changed

Lines changed: 85 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/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)