Summary
A release-tag push for Connect 2026.05.0 triggered the Development workflow alongside the Production workflow. Even though the dev build ran with bakery build --dev-versions only (which skips the release version), the ephemeral dev image definition was assigned a UID identical to the release version's UID. When bakery ci merge later read the build metadata files, it matched the dev-stream build metadata against the release targets and treated the development build as if it were the release build — pushing dev artifacts to the production registries (Docker Hub + GHCR) instead of GHCR-only with a -preview suffix.
This is the same class of bug previously hit with Package Manager, which is why its preview stream was disabled.
Reported by Byonca Honaker while shepherding the Connect May release (Slack thread).
Root cause
ImageTarget.uid() builds the identifier from image name, version, variant, and OS only — it does not include the release stream:
# posit-bakery/posit_bakery/image/image_target.py:285-294
def uid(self) -> str:
"""Generate a unique identifier for the target based on its properties."""
u = f"{self.image_name}-{self.image_version.name}"
if self.image_variant:
u += f"-{self.image_variant.name}"
if self.image_os:
u += f"-{self.image_os.name}"
return re.sub("[ .+/]", "-", u).lower()
The release stream is tracked, but only in image_version.metadata["release_stream"] (ReleaseStreamEnum = release / preview / daily, see posit_bakery/config/image/posit_product/const.py:46-49), not in the UID. Development versions get a distinct render subpath (.dev-{version}, posit_bakery/config/image/dev_version/base.py:258) but the same version name, so:
- release
connect-2026.05.0-<variant>-<os>
- dev (daily/preview)
2026.05.0 → ephemeral .dev-2026.05.0
both produce UID connect-2026.05.0-<variant>-<os>.
bakery ci merge keys build metadata by UID and matches it back onto targets:
# posit_bakery/config/config.py:915-925
def _merge_sequential_build_metadata_files(self) -> dict[str, Any]:
merged_metadata: dict[str, dict[str, Any]] = {}
for target in self.targets:
for build_metadata in target.build_metadata:
merged_metadata[target.uid] = build_metadata.model_dump(exclude_none=True, by_alias=True)
return merged_metadata
# posit_bakery/config/config.py:927-942 (load side)
for target in self.targets:
result = target.load_build_metadata_from_file(metadata_file) # matches by UID
if result is not None:
targets_loaded.append(target.uid)
Because the UID has no stream component, dev metadata silently satisfies the release target lookup.
Impact
- Development-stream builds can be merged into and pushed as release artifacts, overwriting production tags in GHCR (and reaching Docker Hub).
- Requires a manual re-run of the Production build to rewrite the registries after the fact.
- Triggered by the normal
just tag-release flow (push of a release tag), so any product release is exposed to this edge case.
Evidence (Connect May release)
Proposed fix
Differentiate the streams at the UID level and/or fail fast on a collision:
- Include the release stream as a component of the UID (e.g. append
release / preview / daily) so dev and release builds never share an identifier in metadata matching.
- Detect and hard-fail on a collision — if a development version's UID collides with a production version's UID during build/matrix/merge, raise an error rather than silently matching.
Option 1 is the primary fix; option 2 is a useful guardrail regardless.
Acceptance criteria
Related
Summary
A release-tag push for Connect
2026.05.0triggered the Development workflow alongside the Production workflow. Even though the dev build ran withbakery build --dev-versions only(which skips the release version), the ephemeral dev image definition was assigned a UID identical to the release version's UID. Whenbakery ci mergelater read the build metadata files, it matched the dev-stream build metadata against the release targets and treated the development build as if it were the release build — pushing dev artifacts to the production registries (Docker Hub + GHCR) instead of GHCR-only with a-previewsuffix.This is the same class of bug previously hit with Package Manager, which is why its
previewstream was disabled.Reported by Byonca Honaker while shepherding the Connect May release (Slack thread).
Root cause
ImageTarget.uid()builds the identifier from image name, version, variant, and OS only — it does not include the release stream:The release stream is tracked, but only in
image_version.metadata["release_stream"](ReleaseStreamEnum=release/preview/daily, seeposit_bakery/config/image/posit_product/const.py:46-49), not in the UID. Development versions get a distinct render subpath (.dev-{version},posit_bakery/config/image/dev_version/base.py:258) but the same version name, so:connect-2026.05.0-<variant>-<os>2026.05.0→ ephemeral.dev-2026.05.0both produce UID
connect-2026.05.0-<variant>-<os>.bakery ci mergekeys build metadata by UID and matches it back onto targets:Because the UID has no stream component, dev metadata silently satisfies the release target lookup.
Impact
just tag-releaseflow (push of a release tag), so any product release is exposed to this edge case.Evidence (Connect May release)
2026.05.0in the PR build: https://github.com/posit-dev/images-connect/actions/runs/26588343478/job/78341816619#step:14:800Proposed fix
Differentiate the streams at the UID level and/or fail fast on a collision:
release/preview/daily) so dev and release builds never share an identifier in metadata matching.Option 1 is the primary fix; option 2 is a useful guardrail regardless.
Acceptance criteria
Xand a release build of versionXproduce distinct UIDs.bakery ci mergenever matches dev-stream metadata onto release targets (or vice versa).bakery ci matrix/ build /bakery ci merge.Related
previewstream is currently disabled as a workaround.