Skip to content

Commit fa2c180

Browse files
authored
Merge pull request #607 from posit-dev/fix/dev-dep-versions-dev-spec-option
feat: apply --dev-spec to dependency-sourced dev versions
2 parents b3c5d23 + eaff40e commit fa2c180

6 files changed

Lines changed: 136 additions & 20 deletions

File tree

posit-bakery/posit_bakery/config/config.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -401,8 +401,9 @@ def _apply_dev_spec(image: Image, settings: BakerySettings) -> None:
401401
Called before load_dev_versions() so the pinned version is set
402402
when resolution runs.
403403
"""
404-
# Local import to avoid circular import.
404+
# Local imports to avoid circular import.
405405
from posit_bakery.config.image.dev_version.channel import ImageDevelopmentVersionFromProductChannel
406+
from posit_bakery.config.image.dev_version.dependency import ImageDevelopmentVersionFromDependency
406407

407408
# Validate channel consistency: if both are set and differ, the pinned version
408409
# would be filtered out by --dev-channel. Fail loudly instead of silently skipping.
@@ -421,8 +422,8 @@ def _apply_dev_spec(image: Image, settings: BakerySettings) -> None:
421422
candidates = [
422423
dv
423424
for dv in image.devVersions
424-
if isinstance(dv, ImageDevelopmentVersionFromProductChannel)
425-
and (target_channel is None or dv.channel == target_channel)
425+
if isinstance(dv, (ImageDevelopmentVersionFromProductChannel, ImageDevelopmentVersionFromDependency))
426+
and (target_channel is None or dv.get_release_channel() == target_channel)
426427
]
427428
if not candidates:
428429
return
@@ -432,13 +433,24 @@ def _apply_dev_spec(image: Image, settings: BakerySettings) -> None:
432433
f"channel '{target_channel}'. Specify 'channel' in --dev-spec or pass "
433434
f"--dev-channel to disambiguate."
434435
)
435-
candidates[0].version_override = settings.dev_spec.version # None for branch-only specs
436-
if settings.dev_spec.version is not None:
437-
# Extract YYYY.MM from the pinned version to target the correct branch URL.
438-
# The dailies API supports YYYY.MM path segments (e.g. /rstudio/2026.06/index.json).
439-
candidates[0].release_branch = _extract_calver_minor(settings.dev_spec.version)
440-
elif settings.dev_spec.release_branch is not None:
441-
candidates[0].release_branch = settings.dev_spec.release_branch
436+
437+
candidate = candidates[0]
438+
candidate.version_override = settings.dev_spec.version # None for branch-only specs
439+
440+
if isinstance(candidate, ImageDevelopmentVersionFromProductChannel):
441+
if settings.dev_spec.version is not None:
442+
# Extract YYYY.MM from the pinned version to target the correct branch URL.
443+
# The dailies API supports YYYY.MM path segments (e.g. /rstudio/2026.06/index.json).
444+
candidate.release_branch = _extract_calver_minor(settings.dev_spec.version)
445+
elif settings.dev_spec.release_branch is not None:
446+
candidate.release_branch = settings.dev_spec.release_branch
447+
elif settings.dev_spec.version is None:
448+
# Dependency-sourced dev versions resolve from a fixed dependency endpoint
449+
# with no release-branch concept, so a branch-only spec cannot pin them.
450+
log.warning(
451+
f"Image '{image.name}': --dev-spec without a version cannot pin a "
452+
f"dependency-sourced dev version; building the latest resolved version."
453+
)
442454

443455

444456
class BakeryConfig:

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ class BaseImageDevelopmentVersion(BakeryYAMLModel, abc.ABC):
5757
description="Arbitrary key-value pairs used in template rendering.",
5858
),
5959
]
60+
version_override: Annotated[
61+
str | None,
62+
Field(
63+
exclude=True,
64+
default=None,
65+
description="Version pinned by a workflow dispatch spec (--dev-spec). When set, "
66+
"bypasses CDN/dependency discovery so the build targets exactly this version. "
67+
"The stream model also forwards it to the channel resolver for URL construction; "
68+
"the dependency model returns it directly from get_version().",
69+
),
70+
]
6071

6172
@field_validator("extraRegistries", "overrideRegistries", mode="after")
6273
@classmethod

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

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,6 @@ class ImageDevelopmentVersionFromProductChannel(BaseImageDevelopmentVersion):
4141
"for builds targeting older versions.",
4242
),
4343
]
44-
version_override: Annotated[
45-
str | None,
46-
Field(
47-
exclude=True,
48-
default=None,
49-
description="Version pinned by a workflow dispatch spec. When set, bypasses CDN "
50-
"discovery and is forwarded to the channel resolver for offline template rendering "
51-
"(PPM) or manifest assertion (Connect, Workbench).",
52-
),
53-
]
5444
release_branch: Annotated[
5545
str | None,
5646
Field(

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ def channel_not_release(cls, v: ReleaseChannelEnum | None) -> ReleaseChannelEnum
4444
return v
4545

4646
def get_version(self) -> str:
47+
if self.version_override is not None:
48+
return self.version_override
4749
constraint_class = get_dependency_constraint_class(self.dependency)
4850
constraint = constraint_class(
4951
prerelease=self.prerelease,

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,3 +349,24 @@ def test_repr_contains_key_fields(self):
349349
assert 'sourceType="dependency"' in r
350350
assert "positron" in r
351351
assert "prerelease=True" in r
352+
353+
354+
class TestVersionOverride:
355+
def test_version_override_defaults_none(self):
356+
"""The shared version_override field defaults to None."""
357+
dev = ImageDevelopmentVersionFromDependency(
358+
dependency="positron",
359+
os=[_UBUNTU_24_OS],
360+
)
361+
assert dev.version_override is None
362+
363+
def test_version_override_short_circuits_resolution(self):
364+
"""When version_override is set, get_version() returns it without a network call."""
365+
dev = ImageDevelopmentVersionFromDependency(
366+
dependency="positron",
367+
prerelease=True,
368+
os=[_UBUNTU_24_OS],
369+
)
370+
dev.version_override = "2026.06.0-99"
371+
# No patch_requests_get fixture: a network call would error, proving none happens.
372+
assert dev.get_version() == "2026.06.0-99"

posit-bakery/test/config/test_config.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import logging
23
import os
34
import shutil
45
import textwrap
@@ -2329,3 +2330,82 @@ def test_version_takes_precedence_over_release_branch(self, tmp_path):
23292330
def test_extract_calver_minor_rejects_trailing_garbage(self):
23302331
with pytest.raises(ValueError, match="not a valid CalVer"):
23312332
_extract_calver_minor("2026.06.0-daily+143 trailing garbage")
2333+
2334+
2335+
class TestApplyDevSpecDependency:
2336+
"""--dev-spec pins dependency-sourced dev versions, matched by channel."""
2337+
2338+
def _make_image(self, tmp_path, *, channel="daily", extra_dev_versions=None):
2339+
"""Return a minimal Image with one positron daily dependency dev version."""
2340+
dev_version = {
2341+
"sourceType": "dependency",
2342+
"dependency": "positron",
2343+
"prerelease": True,
2344+
"channel": channel,
2345+
"os": [{"name": "Ubuntu 24.04", "primary": True}],
2346+
}
2347+
doc = BakeryConfigDocument(
2348+
base_path=tmp_path,
2349+
**{
2350+
"repository": {"url": "https://github.com/posit-dev/test"},
2351+
"images": [
2352+
{
2353+
"name": "test-positron-init",
2354+
"devVersions": [dev_version, *(extra_dev_versions or [])],
2355+
}
2356+
],
2357+
},
2358+
)
2359+
return doc.images[0]
2360+
2361+
def test_version_pins_dependency_dev_version(self, tmp_path):
2362+
"""version in DevBuildSpec sets version_override on the matching dependency dev version."""
2363+
image = self._make_image(tmp_path)
2364+
spec = DevBuildSpec(version="2026.06.0-99", channel="daily")
2365+
settings = BakerySettings(dev_versions=DevVersionInclusionEnum.ONLY, dev_spec=spec)
2366+
_apply_dev_spec(image, settings)
2367+
assert image.devVersions[0].version_override == "2026.06.0-99"
2368+
2369+
def test_channel_mismatch_is_noop(self, tmp_path):
2370+
"""A dev-spec channel that matches no dev version leaves version_override unset."""
2371+
image = self._make_image(tmp_path, channel="daily")
2372+
spec = DevBuildSpec(version="2026.06.0-99", channel="preview")
2373+
settings = BakerySettings(dev_versions=DevVersionInclusionEnum.ONLY, dev_spec=spec)
2374+
_apply_dev_spec(image, settings)
2375+
assert image.devVersions[0].version_override is None
2376+
2377+
def test_release_branch_ignored_for_dependency(self, tmp_path):
2378+
"""release_branch is not applicable to dependency dev versions and is not set."""
2379+
image = self._make_image(tmp_path)
2380+
spec = DevBuildSpec(version="2026.06.0-99", channel="daily", release_branch="apple-blossom")
2381+
settings = BakerySettings(dev_versions=DevVersionInclusionEnum.ONLY, dev_spec=spec)
2382+
_apply_dev_spec(image, settings)
2383+
dv = image.devVersions[0]
2384+
assert dv.version_override == "2026.06.0-99"
2385+
assert not hasattr(dv, "release_branch")
2386+
2387+
def test_branch_only_spec_skips_dependency_with_warning(self, tmp_path, caplog):
2388+
"""A branch-only spec cannot pin a dependency dev version: warns, leaves override None."""
2389+
image = self._make_image(tmp_path)
2390+
spec = DevBuildSpec(release_branch="apple-blossom")
2391+
settings = BakerySettings(dev_versions=DevVersionInclusionEnum.ONLY, dev_spec=spec)
2392+
with caplog.at_level(logging.WARNING):
2393+
_apply_dev_spec(image, settings)
2394+
assert image.devVersions[0].version_override is None
2395+
warnings = [r for r in caplog.records if r.levelno == logging.WARNING]
2396+
assert len(warnings) == 1
2397+
assert "cannot pin a dependency-sourced dev version" in warnings[0].message
2398+
2399+
def test_ambiguous_candidates_raise(self, tmp_path):
2400+
"""Two dev versions matching the same channel raise a disambiguation error."""
2401+
extra = {
2402+
"sourceType": "stream",
2403+
"product": "workbench",
2404+
"channel": "daily",
2405+
"os": [{"name": "Ubuntu 24.04", "primary": True}],
2406+
}
2407+
image = self._make_image(tmp_path, channel="daily", extra_dev_versions=[extra])
2408+
spec = DevBuildSpec(version="2026.06.0-99", channel="daily")
2409+
settings = BakerySettings(dev_versions=DevVersionInclusionEnum.ONLY, dev_spec=spec)
2410+
with pytest.raises(ValueError, match="dev versions matching"):
2411+
_apply_dev_spec(image, settings)

0 commit comments

Comments
 (0)