Skip to content

Commit 7061ee1

Browse files
authored
Merge pull request #567 from posit-dev/positron-init-preview-stream
Add dependency-backed dev version and Positron daily channel
2 parents b1487c9 + f5c88ac commit 7061ee1

18 files changed

Lines changed: 913 additions & 29 deletions

File tree

posit-bakery/docs/configuration.qmd

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ An Image represents a container image managed by the project. Each image has one
173173
| `versions`<br/>*[ImageVersion](#imageversion) array* | The list of versions for the image. Each version should have its own directory under the image's `subpath`. Cannot be used with `matrix`. | `[]` | `- name: 2025.07.0` |
174174
| `matrix`<br/>*[ImageMatrix](#imagematrix)* | A matrix configuration for generating multiple image versions from dependency combinations. Cannot be used with `versions`. | | See [ImageMatrix](#imagematrix) |
175175
| `options`<br/>*[ToolOptions](#tooloptions) array* | A list of options to pass to a supported tool when performing an action against the image. | `[]` | <pre>- tool: goss<br/> wait: 10<br/> command: "my-custom command"</pre> |
176+
| `devVersions`<br/>*[ImageDevelopmentVersion](#imagedevelopmentversion) array* | Ephemeral versions resolved at build time from a product channel or dependency prerelease. Built when `--dev-versions include` or `--dev-versions only` is passed. | `[]` | See [ImageDevelopmentVersion](#imagedevelopmentversion) |
176177

177178
#### Example Image
178179

@@ -232,6 +233,83 @@ images:
232233
tagDisplayName: ubuntu22.04
233234
```
234235

236+
### ImageDevelopmentVersion
237+
238+
An `ImageDevelopmentVersion` defines an ephemeral version resolved at build time. Unlike static `versions`, it is not stored on disk — Bakery fetches the version string (and optionally download URLs) from an external source each time a dev build runs. Development versions are included when `--dev-versions include` or `--dev-versions only` is passed to `bakery build` or `bakery ci matrix`.
239+
240+
Each entry carries a `sourceType` discriminator that selects the resolution strategy.
241+
242+
**Common fields** (all sourceTypes):
243+
244+
| Field | Description | Default | Example |
245+
|------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|---------|-----------------------------------|
246+
| `sourceType`<br/>*string* | *(Required)* Resolution strategy. `"stream"` resolves from a product release channel; `"dependency"` resolves from a dependency prerelease. | | `stream`, `dependency` |
247+
| `os`<br/>*[ImageVersionOS](#imageversionos) array* | Operating systems to build for. | `[]` | `- name: Ubuntu 24.04` |
248+
| `extraRegistries`<br/>*[Registry](#registry) or [BaseRegistry](#baseregistry) array* | Additional registries to push this dev version to. Cannot be set alongside `overrideRegistries`. | `[]` | |
249+
| `overrideRegistries`<br/>*[Registry](#registry) or [BaseRegistry](#baseregistry) array* | Replaces all inherited registries for this dev version. Cannot be set alongside `extraRegistries`. | `[]` | |
250+
| `values`<br/>*map[string, string]* | Arbitrary key-value pairs passed to Jinja2 templates when rendering this dev version. | `{}` | `POSITRON_CHANNEL: dailies` |
251+
252+
#### `sourceType: stream` — Product Channel
253+
254+
Resolves the version from a Posit product release channel. The artifact download URL is fetched from the channel's CDN and injected into the Containerfile template.
255+
256+
| Field | Description | Example |
257+
|----------------------------|------------------------------------------------------|------------------------------------------------------|
258+
| `product`<br/>*string* | *(Required)* The Posit product ID. | `workbench`, `workbench-session`, `connect`, `package-manager` |
259+
| `channel`<br/>*string* | *(Required)* The release channel. | `daily`, `preview` |
260+
261+
**Example:**
262+
263+
```yaml
264+
devVersions:
265+
- sourceType: stream
266+
product: workbench
267+
channel: daily
268+
overrideRegistries:
269+
- host: ghcr.io
270+
namespace: posit-dev
271+
repository: workbench-preview
272+
os:
273+
- name: Ubuntu 24.04
274+
primary: true
275+
platforms:
276+
- linux/amd64
277+
- linux/arm64
278+
- name: Ubuntu 22.04
279+
```
280+
281+
#### `sourceType: dependency` — Dependency Prerelease
282+
283+
Resolves the version via the dependency constraint system rather than a product channel. The Containerfile template is responsible for constructing the download URL from `Image.Version` and any `values` passed.
284+
285+
| Field | Description | Default | Example |
286+
|-----------------------------|-----------------------------------------------------------------------------------------------------------------------|---------|------------|
287+
| `dependency`<br/>*string* | *(Required)* The dependency to resolve a version for. Must be a supported dependency type. | | `positron` |
288+
| `prerelease`<br/>*bool* | When `true`, includes the dependency's prerelease channel in version resolution. | `false` | `true` |
289+
| `channel`<br/>*string* | The release channel for this dev version. Populates the `{{ Channel }}` tag variable. Omit for stable dependency resolution. `"release"` is not accepted. | *(none)* | `daily`, `preview` |
290+
291+
**Example:**
292+
293+
```yaml
294+
devVersions:
295+
- sourceType: dependency
296+
dependency: positron
297+
prerelease: true
298+
channel: daily
299+
values:
300+
POSITRON_CHANNEL: dailies
301+
overrideRegistries:
302+
- host: ghcr.io
303+
namespace: posit-dev
304+
repository: workbench-positron-init-preview
305+
os:
306+
- name: Ubuntu 24.04
307+
primary: true
308+
platforms:
309+
- linux/amd64
310+
- linux/arm64
311+
```
312+
235313
### ImageVariant
236314

237315
An ImageVariant represents a variant of an image, such as standard or minimal builds. Each variant is expected have its
@@ -329,8 +407,8 @@ At the image level, these are specified through a VersionConstraint.
329407
| `constraint`<br/>*[VersionConstraint](#versionconstraint)* | *(Required)* Constraints to apply to calculate versions. | | <pre>latest: true<br/>count: 2</pre> |
330408

331409
::: {.callout-note}
332-
The `quarto` dependency type supports an additional `prerelease` field (default: `false`).
333-
When `true`, prerelease versions of Quarto are included in the version calculation.
410+
The `quarto` and `positron` dependency types support an additional `prerelease` field (default: `false`).
411+
When `true`, prerelease versions are included in the version calculation.
334412
:::
335413

336414
Each Dependency defines the dependency type, as well as the versions of the dependency that will be installed.

posit-bakery/posit_bakery/cli/ci.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,10 @@ def merge(
224224
Preserved for back-compat. New callers should prefer `bakery ci publish`.
225225
SOCI conversion is driven by per-image/variant `soci` options.
226226
"""
227+
if dev_stream is not None:
228+
log.warning("--dev-stream is deprecated, use --dev-channel instead.")
229+
if dev_channel is None:
230+
dev_channel = dev_stream
227231
publish(
228232
metadata_file=metadata_file,
229233
context=context,

posit-bakery/posit_bakery/config/dependencies/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
# All available Positron releases for Workbench
2525
# The URL contains an architecture segment (x86_64 or arm64).
2626
POSITRON_RELEASES_URL_TEMPLATE = "https://cdn.posit.co/positron/releases/pwb/{arch}/all-releases.json"
27+
# Latest Positron daily build for Workbench
28+
POSITRON_DAILY_URL_TEMPLATE = "https://cdn.posit.co/positron/dailies/pwb/{arch}/releases.json"
2729

2830

2931
@yaml_object(yaml)

posit-bakery/posit_bakery/config/dependencies/positron.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import abc
2-
from typing import Literal, ClassVar
2+
from typing import Annotated, Literal, ClassVar
33

4-
from pydantic import ConfigDict
4+
from pydantic import ConfigDict, Field
55

66
from posit_bakery.config.shared import BakeryYAMLModel
77
from posit_bakery.util import cached_session
8-
from .const import POSITRON_RELEASES_URL_TEMPLATE, SupportedDependencies
8+
from .const import POSITRON_DAILY_URL_TEMPLATE, POSITRON_RELEASES_URL_TEMPLATE, SupportedDependencies
99
from .dependency import DependencyVersions, DependencyConstraint
1010
from .version import DependencyVersion
1111

@@ -21,6 +21,14 @@ class PositronDependency(BakeryYAMLModel, abc.ABC):
2121

2222
dependency: Literal[SupportedDependencies.POSITRON] = SupportedDependencies.POSITRON
2323

24+
prerelease: Annotated[
25+
bool,
26+
Field(
27+
default=False,
28+
description="Whether to include the latest daily build.",
29+
),
30+
]
31+
2432
@staticmethod
2533
def releases_url(target_arch: str = _DEFAULT_ARCH) -> str:
2634
"""Return the releases URL for a given TARGETARCH value.
@@ -31,6 +39,16 @@ def releases_url(target_arch: str = _DEFAULT_ARCH) -> str:
3139
arch = _ARCH_MAP[target_arch]
3240
return POSITRON_RELEASES_URL_TEMPLATE.format(arch=arch)
3341

42+
@staticmethod
43+
def daily_url(target_arch: str = _DEFAULT_ARCH) -> str:
44+
"""Return the daily CDN URL for a given TARGETARCH value.
45+
46+
:param target_arch: Docker TARGETARCH value (amd64 or arm64).
47+
:return: The fully-qualified daily releases URL.
48+
"""
49+
arch = _ARCH_MAP[target_arch]
50+
return POSITRON_DAILY_URL_TEMPLATE.format(arch=arch)
51+
3452
def _fetch_versions(self) -> list[DependencyVersion]:
3553
"""Fetch available Positron versions from Posit CDN.
3654
@@ -48,7 +66,13 @@ def _fetch_versions(self) -> list[DependencyVersion]:
4866
releases = response.json().get("releases", [])
4967
versions = [DependencyVersion(r["version"]) for r in releases]
5068

51-
return sorted(versions, reverse=True)
69+
if self.prerelease:
70+
response = session.get(self.daily_url())
71+
response.raise_for_status()
72+
data = response.json()
73+
versions.append(DependencyVersion(data["version"]))
74+
75+
return sorted(set(versions), reverse=True)
5276

5377
def available_versions(self) -> list[DependencyVersion]:
5478
"""Return a list of available Positron versions.
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
1+
from typing import Annotated, Union
2+
3+
from pydantic import Field
4+
15
from .base import BaseImageDevelopmentVersion
26
from .channel import ImageDevelopmentVersionFromProductChannel
7+
from .dependency import ImageDevelopmentVersionFromDependency
38

4-
DevelopmentVersionTypes = ImageDevelopmentVersionFromProductChannel
5-
DevelopmentVersionField = ImageDevelopmentVersionFromProductChannel
9+
DevelopmentVersionTypes = Union[
10+
ImageDevelopmentVersionFromProductChannel,
11+
ImageDevelopmentVersionFromDependency,
12+
]
13+
DevelopmentVersionField = Annotated[DevelopmentVersionTypes, Field(discriminator="sourceType")]
614

715

816
__all__ = [
917
"BaseImageDevelopmentVersion",
1018
"ImageDevelopmentVersionFromProductChannel",
19+
"ImageDevelopmentVersionFromDependency",
1120
"DevelopmentVersionTypes",
1221
"DevelopmentVersionField",
1322
]
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from typing import Annotated, Literal
2+
3+
from pydantic import Field, field_validator
4+
5+
from posit_bakery.config.dependencies import get_dependency_constraint_class
6+
from posit_bakery.config.dependencies.const import SupportedDependencies
7+
from posit_bakery.config.dependencies.version import VersionConstraint
8+
from posit_bakery.config.image.dev_version.base import BaseImageDevelopmentVersion
9+
from posit_bakery.config.image.posit_product.const import ReleaseChannelEnum
10+
from posit_bakery.config.image.version_os import ImageVersionOS
11+
12+
13+
class ImageDevelopmentVersionFromDependency(BaseImageDevelopmentVersion):
14+
"""Dev version sourced from a dependency constraint.
15+
16+
When ``prerelease=True``, the dependency's prerelease channel is resolved.
17+
Version resolution delegates entirely to the dependency module; the
18+
Containerfile template is responsible for constructing the download URL
19+
from ``Image.Version`` and any values passed via the ``values`` field.
20+
"""
21+
22+
sourceType: Literal["dependency"] = "dependency"
23+
dependency: Annotated[
24+
SupportedDependencies,
25+
Field(description="The dependency to resolve a version for."),
26+
]
27+
prerelease: Annotated[
28+
bool,
29+
Field(default=False, description="Whether to resolve the dependency's prerelease channel."),
30+
] = False
31+
channel: Annotated[
32+
ReleaseChannelEnum | None,
33+
Field(default=None, description="Release channel for this dev version (e.g. 'daily', 'preview')."),
34+
] = None
35+
36+
@field_validator("channel", mode="after")
37+
@classmethod
38+
def channel_not_release(cls, v: ReleaseChannelEnum | None) -> ReleaseChannelEnum | None:
39+
if v == ReleaseChannelEnum.RELEASE:
40+
raise ValueError(
41+
"channel: 'release' is not valid for dependency-sourced dev versions. "
42+
"Omit channel (leave as null) for stable dependency resolution."
43+
)
44+
return v
45+
46+
def get_version(self) -> str:
47+
constraint_class = get_dependency_constraint_class(self.dependency)
48+
constraint = constraint_class(
49+
prerelease=self.prerelease,
50+
constraint=VersionConstraint(latest=True, count=1),
51+
)
52+
result = constraint.resolve_versions()
53+
return str(result.versions[0])
54+
55+
def get_url_by_os(self, generalize_architecture: bool = False) -> dict[str, str]:
56+
return {}
57+
58+
def _resolve_os_urls(self) -> list[ImageVersionOS]:
59+
# URL construction is handled by the Containerfile template via values.
60+
return list(self.os)
61+
62+
def get_release_channel(self) -> ReleaseChannelEnum | None:
63+
return self.channel
64+
65+
def __repr__(self):
66+
return f'devVersion(sourceType="dependency", dependency="{self.dependency}", prerelease={self.prerelease})'

posit-bakery/test/cli/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@
1111
from test.helpers import remove_images
1212

1313

14+
def settings_from_call(mock):
15+
"""Extract BakerySettings from a mocked BakeryConfig.from_context call.
16+
17+
Handles both positional (from_context(context, settings)) and keyword
18+
(from_context(context=context, settings=settings)) calling conventions.
19+
"""
20+
args, kwargs = mock.from_context.call_args
21+
return kwargs.get("settings", args[1] if len(args) > 1 else None)
22+
23+
1424
@pytest.fixture(scope="session")
1525
def ci_testdata():
1626
"""Return the path to the CI test data directory"""

posit-bakery/test/cli/test_dev_spec.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,12 @@
88
from posit_bakery.cli.main import app
99
from posit_bakery.config.image.dev_version.spec import DevBuildSpec
1010
from posit_bakery.config.image.posit_product.const import ReleaseChannelEnum
11+
from test.cli.conftest import settings_from_call
1112

1213
runner = CliRunner()
1314
BASIC_CONTEXT = str(Path(__file__).parent.parent / "resources" / "basic")
1415

1516

16-
def _settings_from_call(mock):
17-
"""Extract the BakerySettings passed to BakeryConfig.from_context.
18-
19-
Handles both positional (build.py: from_context(context, settings))
20-
and keyword (ci.py: from_context(context=context, settings=settings)) calls.
21-
"""
22-
args, kwargs = mock.from_context.call_args
23-
return kwargs.get("settings", args[1] if len(args) > 1 else None)
24-
25-
2617
class TestBuildDevSpec:
2718
def test_dev_spec_via_flag(self):
2819
"""--dev-spec JSON is parsed and forwarded to BakerySettings."""
@@ -43,7 +34,7 @@ def test_dev_spec_via_flag(self):
4334
catch_exceptions=False,
4435
)
4536
assert result.exit_code == 0, result.output
46-
settings = _settings_from_call(mock)
37+
settings = settings_from_call(mock)
4738
assert isinstance(settings.dev_spec, DevBuildSpec)
4839
assert settings.dev_spec.version == "2026.05.0-dev+185-gSHA"
4940
assert settings.dev_spec.channel == ReleaseChannelEnum.DAILY
@@ -60,7 +51,7 @@ def test_dev_spec_via_env_var(self):
6051
catch_exceptions=False,
6152
)
6253
assert result.exit_code == 0, result.output
63-
settings = _settings_from_call(mock)
54+
settings = settings_from_call(mock)
6455
assert isinstance(settings.dev_spec, DevBuildSpec)
6556
assert settings.dev_spec.version == "2026.05.0-dev+185-gSHA"
6657
assert settings.dev_spec.channel is None
@@ -102,7 +93,7 @@ def test_dev_spec_absent_is_none(self):
10293
catch_exceptions=False,
10394
)
10495
assert result.exit_code == 0, result.output
105-
settings = _settings_from_call(mock)
96+
settings = settings_from_call(mock)
10697
assert settings.dev_spec is None
10798

10899

@@ -128,7 +119,7 @@ def test_dev_spec_via_flag(self):
128119
catch_exceptions=False,
129120
)
130121
assert result.exit_code == 0, result.output
131-
settings = _settings_from_call(mock)
122+
settings = settings_from_call(mock)
132123
assert isinstance(settings.dev_spec, DevBuildSpec)
133124
assert settings.dev_spec.version == "2026.05.0-dev+185-gSHA"
134125
assert settings.dev_spec.channel == ReleaseChannelEnum.DAILY
@@ -146,7 +137,7 @@ def test_dev_spec_via_env_var(self):
146137
catch_exceptions=False,
147138
)
148139
assert result.exit_code == 0, result.output
149-
settings = _settings_from_call(mock)
140+
settings = settings_from_call(mock)
150141
assert isinstance(settings.dev_spec, DevBuildSpec)
151142
assert settings.dev_spec.version == "2026.05.0-dev+185-gSHA"
152143
assert settings.dev_spec.channel is None
@@ -190,5 +181,5 @@ def test_dev_spec_absent_is_none(self):
190181
catch_exceptions=False,
191182
)
192183
assert result.exit_code == 0, result.output
193-
settings = _settings_from_call(mock)
184+
settings = settings_from_call(mock)
194185
assert settings.dev_spec is None

0 commit comments

Comments
 (0)