diff --git a/.github/workflows/bakery-build-native.yml b/.github/workflows/bakery-build-native.yml index a4eaec5f..2a564ee6 100644 --- a/.github/workflows/bakery-build-native.yml +++ b/.github/workflows/bakery-build-native.yml @@ -140,25 +140,17 @@ jobs: with: version: ${{ inputs.version }} - - name: Images by Version/Platform - id: images-by-platform + - name: Resolve dev-spec + id: resolve-dev-spec env: - DEV_VERSIONS: ${{ inputs.dev-versions }} - MATRIX_VERSIONS: ${{ inputs.matrix-versions }} - IMAGE_NAME_FILTER: ${{ inputs.image-name }} - IMAGE_VERSION: ${{ inputs.image-version }} - DEV_CHANNEL: ${{ inputs.dev-channel || inputs.dev-stream }} - DEV_VERSION: ${{ inputs.dev-version }} DEV_SPEC: ${{ inputs.dev-spec }} + DEV_VERSION: ${{ inputs.dev-version }} + DEV_CHANNEL: ${{ inputs.dev-channel || inputs.dev-stream }} RELEASE_BRANCH: ${{ inputs.release-branch }} - CONTEXT: ${{ inputs.context }} run: | - IMAGE_VERSION="${IMAGE_VERSION#v}" - ARGS=(--quiet --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS" --context "$CONTEXT") - [[ -n "$IMAGE_VERSION" ]] && ARGS+=(--image-version "$IMAGE_VERSION") - [[ -n "$IMAGE_NAME_FILTER" ]] && ARGS+=(-- "$IMAGE_NAME_FILTER") if [[ -n "$DEV_SPEC" ]]; then - ARGS+=(--dev-spec "$DEV_SPEC") + DEV_SPEC_COMPACT=$(echo "$DEV_SPEC" | jq -c .) + echo "dev-spec=$DEV_SPEC_COMPACT" >> "$GITHUB_OUTPUT" elif [[ -n "$DEV_VERSION" || -n "$RELEASE_BRANCH" ]]; then DEV_SPEC_JSON=$(jq -cn \ --arg v "$DEV_VERSION" \ @@ -167,10 +159,31 @@ jobs: 'if $v != "" then {version: $v} else {} end + if $c != "" then {channel: $c} else {} end + if $b != "" then {release_branch: $b} else {} end') - ARGS+=(--dev-spec "$DEV_SPEC_JSON") + echo "dev-spec=$DEV_SPEC_JSON" >> "$GITHUB_OUTPUT" + else + echo "dev-spec=" >> "$GITHUB_OUTPUT" + fi + + - name: Images by Version/Platform + id: images-by-platform + env: + DEV_VERSIONS: ${{ inputs.dev-versions }} + MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + IMAGE_NAME_FILTER: ${{ inputs.image-name }} + IMAGE_VERSION: ${{ inputs.image-version }} + DEV_CHANNEL: ${{ inputs.dev-channel || inputs.dev-stream }} + DEV_SPEC_RESOLVED: ${{ steps.resolve-dev-spec.outputs.dev-spec }} + CONTEXT: ${{ inputs.context }} + run: | + IMAGE_VERSION="${IMAGE_VERSION#v}" + ARGS=(--quiet --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS" --context "$CONTEXT") + [[ -n "$IMAGE_VERSION" ]] && ARGS+=(--image-version "$IMAGE_VERSION") + if [[ -n "$DEV_SPEC_RESOLVED" ]]; then + ARGS+=(--dev-spec "$DEV_SPEC_RESOLVED") elif [[ -n "$DEV_CHANNEL" ]]; then ARGS+=(--dev-channel "$DEV_CHANNEL") fi + [[ -n "$IMAGE_NAME_FILTER" ]] && ARGS+=(-- "$IMAGE_NAME_FILTER") result=$(bakery ci matrix "${ARGS[@]}") echo "platform_matrix=$(echo "$result" | jq --compact-output .)" >> "$GITHUB_OUTPUT" echo "image_matrix=$(echo "$result" | jq --compact-output '[.[].image] | unique')" >> "$GITHUB_OUTPUT" @@ -258,6 +271,29 @@ jobs: run: | PLATFORM=${BUILD_PLATFORM#linux/} echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + - name: Resolve dev-spec + id: resolve-dev-spec + env: + DEV_SPEC: ${{ inputs.dev-spec }} + DEV_VERSION: ${{ inputs.dev-version }} + DEV_CHANNEL: ${{ inputs.dev-channel || inputs.dev-stream }} + RELEASE_BRANCH: ${{ inputs.release-branch }} + run: | + if [[ -n "$DEV_SPEC" ]]; then + DEV_SPEC_COMPACT=$(echo "$DEV_SPEC" | jq -c .) + echo "dev-spec=$DEV_SPEC_COMPACT" >> "$GITHUB_OUTPUT" + elif [[ -n "$DEV_VERSION" || -n "$RELEASE_BRANCH" ]]; then + DEV_SPEC_JSON=$(jq -cn \ + --arg v "$DEV_VERSION" \ + --arg c "$DEV_CHANNEL" \ + --arg b "$RELEASE_BRANCH" \ + 'if $v != "" then {version: $v} else {} end + + if $c != "" then {channel: $c} else {} end + + if $b != "" then {release_branch: $b} else {} end') + echo "dev-spec=$DEV_SPEC_JSON" >> "$GITHUB_OUTPUT" + else + echo "dev-spec=" >> "$GITHUB_OUTPUT" + fi - name: Build env: GIT_SHA: ${{ github.sha }} @@ -267,10 +303,7 @@ jobs: IMAGE_PLATFORM: ${{ matrix.img.platform }} DEV_VERSIONS: ${{ inputs.dev-versions }} MATRIX_VERSIONS: ${{ inputs.matrix-versions }} - DEV_CHANNEL: ${{ inputs.dev-channel || inputs.dev-stream }} - DEV_VERSION: ${{ inputs.dev-version }} - DEV_SPEC: ${{ inputs.dev-spec }} - RELEASE_BRANCH: ${{ inputs.release-branch }} + DEV_SPEC_RESOLVED: ${{ steps.resolve-dev-spec.outputs.dev-spec }} REGISTRY: ghcr.io/${{ github.repository_owner }} NORMALIZED_PLATFORM: ${{ steps.normalize-platform.outputs.platform }} CONTEXT: ${{ inputs.context }} @@ -283,18 +316,7 @@ jobs: CACHE_FLAGS=(--cache-registry "$REGISTRY") fi DEV_SPEC_FLAGS=() - if [[ -n "$DEV_SPEC" ]]; then - DEV_SPEC_FLAGS+=(--dev-spec "$DEV_SPEC") - elif [[ -n "$DEV_VERSION" || -n "$RELEASE_BRANCH" ]]; then - DEV_SPEC_JSON=$(jq -cn \ - --arg v "$DEV_VERSION" \ - --arg c "$DEV_CHANNEL" \ - --arg b "$RELEASE_BRANCH" \ - 'if $v != "" then {version: $v} else {} end - + if $c != "" then {channel: $c} else {} end - + if $b != "" then {release_branch: $b} else {} end') - DEV_SPEC_FLAGS+=(--dev-spec "$DEV_SPEC_JSON") - fi + [[ -n "$DEV_SPEC_RESOLVED" ]] && DEV_SPEC_FLAGS=(--dev-spec "$DEV_SPEC_RESOLVED") bakery build \ --strategy build --pull \ --retry "$RETRY" \ @@ -316,10 +338,13 @@ jobs: IMAGE_PLATFORM: ${{ matrix.img.platform }} DEV_VERSIONS: ${{ inputs.dev-versions }} MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + DEV_SPEC_RESOLVED: ${{ steps.resolve-dev-spec.outputs.dev-spec }} NORMALIZED_PLATFORM: ${{ steps.normalize-platform.outputs.platform }} CONTEXT: ${{ inputs.context }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | + DEV_SPEC_FLAGS=() + [[ -n "$DEV_SPEC_RESOLVED" ]] && DEV_SPEC_FLAGS=(--dev-spec "$DEV_SPEC_RESOLVED") GOSS_PATH=${GITHUB_WORKSPACE}/tools/goss \ DGOSS_PATH=${GITHUB_WORKSPACE}/tools/dgoss \ bakery dgoss run \ @@ -328,6 +353,7 @@ jobs: --image-platform "$IMAGE_PLATFORM" \ --dev-versions "$DEV_VERSIONS" \ --matrix-versions "$MATRIX_VERSIONS" \ + "${DEV_SPEC_FLAGS[@]}" \ --metadata-file "./${IMAGE_NAME}-${IMAGE_VERSION}-${NORMALIZED_PLATFORM}-metadata.json" \ --context "$CONTEXT" - name: Upload Metadata @@ -424,6 +450,30 @@ jobs: run: | ls -ltra . + - name: Resolve dev-spec + id: resolve-dev-spec + env: + DEV_SPEC: ${{ inputs.dev-spec }} + DEV_VERSION: ${{ inputs.dev-version }} + DEV_CHANNEL: ${{ inputs.dev-channel || inputs.dev-stream }} + RELEASE_BRANCH: ${{ inputs.release-branch }} + run: | + if [[ -n "$DEV_SPEC" ]]; then + DEV_SPEC_COMPACT=$(echo "$DEV_SPEC" | jq -c .) + echo "dev-spec=$DEV_SPEC_COMPACT" >> "$GITHUB_OUTPUT" + elif [[ -n "$DEV_VERSION" || -n "$RELEASE_BRANCH" ]]; then + DEV_SPEC_JSON=$(jq -cn \ + --arg v "$DEV_VERSION" \ + --arg c "$DEV_CHANNEL" \ + --arg b "$RELEASE_BRANCH" \ + 'if $v != "" then {version: $v} else {} end + + if $c != "" then {channel: $c} else {} end + + if $b != "" then {release_branch: $b} else {} end') + echo "dev-spec=$DEV_SPEC_JSON" >> "$GITHUB_OUTPUT" + else + echo "dev-spec=" >> "$GITHUB_OUTPUT" + fi + - name: Publish env: GIT_SHA: ${{ github.sha }} @@ -431,12 +481,16 @@ jobs: REGISTRY: ghcr.io/${{ github.repository_owner }} PUSH: ${{ inputs.push }} IMAGE_NAME: ${{ matrix.image }} + DEV_SPEC_RESOLVED: ${{ steps.resolve-dev-spec.outputs.dev-spec }} run: | if [ "$PUSH" = "true" ]; then PUSH_FLAG=""; else PUSH_FLAG="--dry-run"; fi + DEV_SPEC_FLAGS=() + [[ -n "$DEV_SPEC_RESOLVED" ]] && DEV_SPEC_FLAGS=(--dev-spec "$DEV_SPEC_RESOLVED") bakery ci publish \ --context "$CONTEXT" \ --image-name "^${IMAGE_NAME}$" \ --temp-registry "$REGISTRY" \ + "${DEV_SPEC_FLAGS[@]}" \ $PUSH_FLAG \ ./*-metadata.json diff --git a/.github/workflows/bakery-build.yml b/.github/workflows/bakery-build.yml index 26fcbd86..ad629660 100644 --- a/.github/workflows/bakery-build.yml +++ b/.github/workflows/bakery-build.yml @@ -131,6 +131,24 @@ jobs: with: version: ${{ inputs.version }} + - name: Resolve dev-spec + id: resolve-dev-spec + env: + DEV_SPEC: ${{ inputs.dev-spec }} + DEV_VERSION: ${{ inputs.dev-version }} + DEV_CHANNEL: ${{ inputs.dev-channel || inputs.dev-stream }} + run: | + if [[ -n "$DEV_SPEC" ]]; then + DEV_SPEC_COMPACT=$(echo "$DEV_SPEC" | jq -c .) + echo "dev-spec=$DEV_SPEC_COMPACT" >> "$GITHUB_OUTPUT" + elif [[ -n "$DEV_VERSION" ]]; then + DEV_SPEC_JSON=$(jq -cn --arg v "$DEV_VERSION" --arg c "$DEV_CHANNEL" \ + '{version: $v} + if $c != "" then {channel: $c} else {} end') + echo "dev-spec=$DEV_SPEC_JSON" >> "$GITHUB_OUTPUT" + else + echo "dev-spec=" >> "$GITHUB_OUTPUT" + fi + - name: Images id: images env: @@ -139,23 +157,18 @@ jobs: IMAGE_NAME_FILTER: ${{ inputs.image-name }} IMAGE_VERSION: ${{ inputs.image-version }} DEV_CHANNEL: ${{ inputs.dev-channel || inputs.dev-stream }} - DEV_VERSION: ${{ inputs.dev-version }} - DEV_SPEC: ${{ inputs.dev-spec }} + DEV_SPEC_RESOLVED: ${{ steps.resolve-dev-spec.outputs.dev-spec }} CONTEXT: ${{ inputs.context }} run: | IMAGE_VERSION="${IMAGE_VERSION#v}" ARGS=(--quiet --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS" --context "$CONTEXT") [[ -n "$IMAGE_VERSION" ]] && ARGS+=(--image-version "$IMAGE_VERSION") - [[ -n "$IMAGE_NAME_FILTER" ]] && ARGS+=(-- "$IMAGE_NAME_FILTER") - if [[ -n "$DEV_SPEC" ]]; then - ARGS+=(--dev-spec "$DEV_SPEC") - elif [[ -n "$DEV_VERSION" ]]; then - DEV_SPEC_JSON=$(jq -cn --arg v "$DEV_VERSION" --arg c "$DEV_CHANNEL" \ - '{version: $v} + if $c != "" then {channel: $c} else {} end') - ARGS+=(--dev-spec "$DEV_SPEC_JSON") + if [[ -n "$DEV_SPEC_RESOLVED" ]]; then + ARGS+=(--dev-spec "$DEV_SPEC_RESOLVED") elif [[ -n "$DEV_CHANNEL" ]]; then ARGS+=(--dev-channel "$DEV_CHANNEL") fi + [[ -n "$IMAGE_NAME_FILTER" ]] && ARGS+=(-- "$IMAGE_NAME_FILTER") result=$(bakery ci matrix "${ARGS[@]}") echo "matrix=$(echo "$result" | jq --compact-output .)" >> "$GITHUB_OUTPUT" @@ -226,6 +239,24 @@ jobs: - name: Setup docker buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - name: Resolve dev-spec + id: resolve-dev-spec + env: + DEV_SPEC: ${{ inputs.dev-spec }} + DEV_VERSION: ${{ inputs.dev-version }} + DEV_CHANNEL: ${{ inputs.dev-channel || inputs.dev-stream }} + run: | + if [[ -n "$DEV_SPEC" ]]; then + DEV_SPEC_COMPACT=$(echo "$DEV_SPEC" | jq -c .) + echo "dev-spec=$DEV_SPEC_COMPACT" >> "$GITHUB_OUTPUT" + elif [[ -n "$DEV_VERSION" ]]; then + DEV_SPEC_JSON=$(jq -cn --arg v "$DEV_VERSION" --arg c "$DEV_CHANNEL" \ + '{version: $v} + if $c != "" then {channel: $c} else {} end') + echo "dev-spec=$DEV_SPEC_JSON" >> "$GITHUB_OUTPUT" + else + echo "dev-spec=" >> "$GITHUB_OUTPUT" + fi + - name: Build env: GIT_SHA: ${{ github.sha }} @@ -234,9 +265,7 @@ jobs: IMAGE_VERSION: ${{ matrix.img.version }} DEV_VERSIONS: ${{ inputs.dev-versions }} MATRIX_VERSIONS: ${{ inputs.matrix-versions }} - DEV_CHANNEL: ${{ inputs.dev-channel || inputs.dev-stream }} - DEV_VERSION: ${{ inputs.dev-version }} - DEV_SPEC: ${{ inputs.dev-spec }} + DEV_SPEC_RESOLVED: ${{ steps.resolve-dev-spec.outputs.dev-spec }} REGISTRY: ghcr.io/${{ github.repository_owner }} CONTEXT: ${{ inputs.context }} CACHE: ${{ inputs.cache }} @@ -247,13 +276,7 @@ jobs: CACHE_FLAGS=(--cache-registry "$REGISTRY") fi DEV_SPEC_FLAGS=() - if [[ -n "$DEV_SPEC" ]]; then - DEV_SPEC_FLAGS+=(--dev-spec "$DEV_SPEC") - elif [[ -n "$DEV_VERSION" ]]; then - DEV_SPEC_JSON=$(jq -cn --arg v "$DEV_VERSION" --arg c "$DEV_CHANNEL" \ - '{version: $v} + if $c != "" then {channel: $c} else {} end') - DEV_SPEC_FLAGS+=(--dev-spec "$DEV_SPEC_JSON") - fi + [[ -n "$DEV_SPEC_RESOLVED" ]] && DEV_SPEC_FLAGS=(--dev-spec "$DEV_SPEC_RESOLVED") bakery build --load --pull \ --retry "$RETRY" \ --image-name "^${IMAGE_NAME}$" \ @@ -270,9 +293,12 @@ jobs: IMAGE_VERSION: ${{ matrix.img.version }} DEV_VERSIONS: ${{ inputs.dev-versions }} MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + DEV_SPEC_RESOLVED: ${{ steps.resolve-dev-spec.outputs.dev-spec }} CONTEXT: ${{ inputs.context }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | + DEV_SPEC_FLAGS=() + [[ -n "$DEV_SPEC_RESOLVED" ]] && DEV_SPEC_FLAGS=(--dev-spec "$DEV_SPEC_RESOLVED") GOSS_PATH=${GITHUB_WORKSPACE}/tools/goss \ DGOSS_PATH=${GITHUB_WORKSPACE}/tools/dgoss \ bakery dgoss run \ @@ -280,6 +306,7 @@ jobs: --image-version "$IMAGE_VERSION" \ --dev-versions "$DEV_VERSIONS" \ --matrix-versions "$MATRIX_VERSIONS" \ + "${DEV_SPEC_FLAGS[@]}" \ --context "$CONTEXT" - name: Push @@ -294,20 +321,12 @@ jobs: IMAGE_VERSION: ${{ matrix.img.version }} DEV_VERSIONS: ${{ inputs.dev-versions }} MATRIX_VERSIONS: ${{ inputs.matrix-versions }} - DEV_CHANNEL: ${{ inputs.dev-channel || inputs.dev-stream }} - DEV_VERSION: ${{ inputs.dev-version }} - DEV_SPEC: ${{ inputs.dev-spec }} + DEV_SPEC_RESOLVED: ${{ steps.resolve-dev-spec.outputs.dev-spec }} CONTEXT: ${{ inputs.context }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | DEV_SPEC_FLAGS=() - if [[ -n "$DEV_SPEC" ]]; then - DEV_SPEC_FLAGS+=(--dev-spec "$DEV_SPEC") - elif [[ -n "$DEV_VERSION" ]]; then - DEV_SPEC_JSON=$(jq -cn --arg v "$DEV_VERSION" --arg c "$DEV_CHANNEL" \ - '{version: $v} + if $c != "" then {channel: $c} else {} end') - DEV_SPEC_FLAGS+=(--dev-spec "$DEV_SPEC_JSON") - fi + [[ -n "$DEV_SPEC_RESOLVED" ]] && DEV_SPEC_FLAGS=(--dev-spec "$DEV_SPEC_RESOLVED") bakery build --push --pull \ --retry "$RETRY" \ --image-name "^${IMAGE_NAME}$" \ diff --git a/posit-bakery/posit_bakery/cli/ci.py b/posit-bakery/posit_bakery/cli/ci.py index a7c8b4c7..7b018f1b 100644 --- a/posit-bakery/posit_bakery/cli/ci.py +++ b/posit-bakery/posit_bakery/cli/ci.py @@ -217,6 +217,16 @@ def merge( rich_help_panel=RichHelpPanelEnum.FILTERS, ), ] = None, + dev_spec: Annotated[ + str | None, + typer.Option( + "--dev-spec", + envvar="BAKERY_DEV_SPEC", + help='JSON spec for a dispatched dev build. Ex: \'{"version": "2026.05.0-dev+185-gSHA", "channel": "daily"}\'', + rich_help_panel=RichHelpPanelEnum.FILTERS, + callback=parse_dev_spec, + ), + ] = None, ): """Alias for `bakery ci publish`. @@ -234,6 +244,7 @@ def merge( temp_registry=temp_registry, dry_run=dry_run, dev_channel=dev_channel, + dev_spec=dev_spec, # type: ignore[arg-type] # typer requires str annotation; parse_dev_spec callback delivers DevBuildSpec at runtime ) @@ -275,6 +286,16 @@ def publish( rich_help_panel=RichHelpPanelEnum.FILTERS, ), ] = None, + dev_spec: Annotated[ + str | None, + typer.Option( + "--dev-spec", + envvar="BAKERY_DEV_SPEC", + help='JSON spec for a dispatched dev build. Ex: \'{"version": "2026.05.0-dev+185-gSHA", "channel": "daily"}\'', + rich_help_panel=RichHelpPanelEnum.FILTERS, + callback=parse_dev_spec, + ), + ] = None, ) -> None: """Publish multi-platform images by composing oras index-create → soci-convert → oras index-copy. @@ -310,6 +331,7 @@ def publish( filter=BakeryConfigFilter(image_name=image_name), dev_versions=DevVersionInclusionEnum.INCLUDE, dev_channel=dev_channel, + dev_spec=dev_spec, # type: ignore[arg-type] # typer requires str annotation; parse_dev_spec callback delivers DevBuildSpec at runtime matrix_versions=MatrixVersionInclusionEnum.INCLUDE, clean_temporary=False, temp_registry=temp_registry, diff --git a/posit-bakery/posit_bakery/cli/run.py b/posit-bakery/posit_bakery/cli/run.py index ad1752da..c1f64411 100644 --- a/posit-bakery/posit_bakery/cli/run.py +++ b/posit-bakery/posit_bakery/cli/run.py @@ -6,7 +6,7 @@ import typer -from posit_bakery.cli.common import with_verbosity_flags +from posit_bakery.cli.common import with_verbosity_flags, parse_dev_spec from posit_bakery.config import BakeryConfig from posit_bakery.config.config import BakeryConfigFilter, BakerySettings from posit_bakery.config.image.posit_product.const import ReleaseChannelEnum @@ -107,6 +107,16 @@ def dgoss( rich_help_panel=RichHelpPanelEnum.FILTERS, ), ] = None, + dev_spec: Annotated[ + str | None, + typer.Option( + "--dev-spec", + envvar="BAKERY_DEV_SPEC", + help='JSON spec for a dispatched dev build. Ex: \'{"version": "2026.05.0-dev+185-gSHA", "channel": "daily"}\'', + rich_help_panel=RichHelpPanelEnum.FILTERS, + callback=parse_dev_spec, + ), + ] = None, matrix_versions: Annotated[ Optional[MatrixVersionInclusionEnum], typer.Option( @@ -180,6 +190,7 @@ def dgoss( ), dev_versions=dev_versions, dev_channel=dev_channel, + dev_spec=dev_spec, # type: ignore[arg-type] # typer requires str annotation; parse_dev_spec callback delivers DevBuildSpec at runtime matrix_versions=matrix_versions, latest=latest, clean_temporary=clean, diff --git a/posit-bakery/posit_bakery/plugins/builtin/dgoss/__init__.py b/posit-bakery/posit_bakery/plugins/builtin/dgoss/__init__.py index e52bdbae..35fd424b 100644 --- a/posit-bakery/posit_bakery/plugins/builtin/dgoss/__init__.py +++ b/posit-bakery/posit_bakery/plugins/builtin/dgoss/__init__.py @@ -7,7 +7,8 @@ import typer -from posit_bakery.cli.common import with_verbosity_flags +from posit_bakery.cli.common import with_verbosity_flags, parse_dev_spec +from posit_bakery.config.image.posit_product.const import ReleaseChannelEnum from posit_bakery.config.config import BakeryConfig, BakeryConfigFilter, BakerySettings from posit_bakery.const import DevVersionInclusionEnum, MatrixVersionInclusionEnum from posit_bakery.error import BakeryToolRuntimeErrorGroup @@ -104,6 +105,33 @@ def run( rich_help_panel=RichHelpPanelEnum.FILTERS, ), ] = DevVersionInclusionEnum.EXCLUDE, + dev_channel: Annotated[ + Optional[ReleaseChannelEnum], + typer.Option( + "--dev-channel", + help="Filter development versions to a specific release channel.", + rich_help_panel=RichHelpPanelEnum.FILTERS, + ), + ] = None, + dev_stream: Annotated[ + Optional[ReleaseChannelEnum], + typer.Option( + "--dev-stream", + help="Deprecated: use --dev-channel instead.", + hidden=True, + rich_help_panel=RichHelpPanelEnum.FILTERS, + ), + ] = None, + dev_spec: Annotated[ + str | None, + typer.Option( + "--dev-spec", + envvar="BAKERY_DEV_SPEC", + help='JSON spec for a dispatched dev build. Ex: \'{"version": "2026.05.0-dev+185-gSHA", "channel": "daily"}\'', + rich_help_panel=RichHelpPanelEnum.FILTERS, + callback=parse_dev_spec, + ), + ] = None, matrix_versions: Annotated[ Optional[MatrixVersionInclusionEnum], typer.Option( @@ -162,6 +190,10 @@ def run( if not platform.startswith("linux/"): platform = f"linux/{platform}" + if dev_stream is not None: + log.warning("--dev-stream is deprecated, use --dev-channel instead.") + if dev_channel is None: + dev_channel = dev_stream settings = BakerySettings( filter=BakeryConfigFilter( image_name=image_name, @@ -171,6 +203,8 @@ def run( image_platform=[platform], ), dev_versions=dev_versions, + dev_channel=dev_channel, + dev_spec=dev_spec, # type: ignore[arg-type] # typer requires str annotation; parse_dev_spec callback delivers DevBuildSpec at runtime matrix_versions=matrix_versions, latest=latest, clean_temporary=clean, diff --git a/posit-bakery/test/cli/test_dev_spec.py b/posit-bakery/test/cli/test_dev_spec.py index e6a85e00..9216736d 100644 --- a/posit-bakery/test/cli/test_dev_spec.py +++ b/posit-bakery/test/cli/test_dev_spec.py @@ -183,3 +183,199 @@ def test_dev_spec_absent_is_none(self): assert result.exit_code == 0, result.output settings = settings_from_call(mock) assert settings.dev_spec is None + + +class TestDgossRunDevSpec: + """Tests for --dev-spec / BAKERY_DEV_SPEC in bakery dgoss run.""" + + def _invoke(self, extra_args: list[str], env: dict | None = None): + with ( + patch("posit_bakery.plugins.builtin.dgoss.BakeryConfig") as mock_config, + patch("posit_bakery.plugins.builtin.dgoss.DGossPlugin.execute", return_value=[]), + patch("posit_bakery.plugins.builtin.dgoss.DGossPlugin.results"), + ): + instance = MagicMock() + instance.base_path = Path(BASIC_CONTEXT) + instance.targets = [] + mock_config.from_context.return_value = instance + result = runner.invoke( + app, + ["dgoss", "run", "--context", BASIC_CONTEXT] + extra_args, + env=env, + catch_exceptions=False, + ) + return result, mock_config + + def test_dev_spec_via_flag(self): + """--dev-spec JSON is parsed and forwarded to BakerySettings in dgoss run.""" + result, mock = self._invoke( + ["--dev-versions", "only", "--dev-spec", '{"version": "2026.05.0-dev+185-gSHA", "channel": "daily"}'] + ) + assert result.exit_code == 0, result.output + settings = settings_from_call(mock) + assert isinstance(settings.dev_spec, DevBuildSpec) + assert settings.dev_spec.version == "2026.05.0-dev+185-gSHA" + assert settings.dev_spec.channel == ReleaseChannelEnum.DAILY + + def test_dev_spec_via_env_var(self): + """BAKERY_DEV_SPEC env var works in dgoss run.""" + result, mock = self._invoke( + ["--dev-versions", "only"], + env={"BAKERY_DEV_SPEC": '{"version": "2026.05.0-dev+185-gSHA"}'}, + ) + assert result.exit_code == 0, result.output + settings = settings_from_call(mock) + assert isinstance(settings.dev_spec, DevBuildSpec) + assert settings.dev_spec.version == "2026.05.0-dev+185-gSHA" + assert settings.dev_spec.channel is None + + def test_dev_spec_absent_is_none(self): + """When --dev-spec is absent, BakerySettings.dev_spec is None.""" + result, mock = self._invoke([]) + assert result.exit_code == 0, result.output + settings = settings_from_call(mock) + assert settings.dev_spec is None + + def test_dev_spec_invalid_json_rejected(self): + """Invalid JSON in --dev-spec causes a non-zero exit.""" + result, _ = self._invoke(["--dev-spec", "not-json"]) + assert result.exit_code != 0 + + def test_dev_spec_invalid_schema_rejected(self): + """JSON with unknown fields is rejected in dgoss run (extra='forbid' on DevBuildSpec).""" + result, _ = self._invoke(["--dev-spec", '{"version": "1.0.0", "chanenl": "daily"}']) + assert result.exit_code != 0 + + def test_dev_channel_forwarded_to_settings(self): + """--dev-channel is forwarded to BakerySettings.dev_channel.""" + result, mock = self._invoke(["--dev-versions", "only", "--dev-channel", "daily"]) + assert result.exit_code == 0, result.output + settings = settings_from_call(mock) + assert settings.dev_channel == ReleaseChannelEnum.DAILY + + +class TestRunDgossDevSpec: + """Tests for --dev-spec / BAKERY_DEV_SPEC in the deprecated bakery run dgoss.""" + + def _invoke(self, extra_args: list[str], env: dict | None = None): + with ( + patch("posit_bakery.cli.run.BakeryConfig") as mock_config, + patch("posit_bakery.cli.run.get_plugin") as mock_get_plugin, + ): + instance = MagicMock() + instance.base_path = Path(BASIC_CONTEXT) + instance.targets = [] + mock_config.from_context.return_value = instance + mock_plugin = MagicMock() + mock_plugin.execute.return_value = [] + mock_get_plugin.return_value = mock_plugin + result = runner.invoke( + app, + ["run", "dgoss", "--context", BASIC_CONTEXT] + extra_args, + env=env, + catch_exceptions=False, + ) + return result, mock_config + + def test_dev_spec_via_flag(self): + """--dev-spec JSON is parsed and forwarded to BakerySettings in run dgoss.""" + result, mock = self._invoke( + ["--dev-versions", "only", "--dev-spec", '{"version": "2026.05.0-dev+185-gSHA", "channel": "daily"}'] + ) + assert result.exit_code == 0, result.output + settings = settings_from_call(mock) + assert isinstance(settings.dev_spec, DevBuildSpec) + assert settings.dev_spec.version == "2026.05.0-dev+185-gSHA" + assert settings.dev_spec.channel == ReleaseChannelEnum.DAILY + + def test_dev_spec_via_env_var(self): + """BAKERY_DEV_SPEC env var works in run dgoss.""" + result, mock = self._invoke( + ["--dev-versions", "only"], + env={"BAKERY_DEV_SPEC": '{"version": "2026.05.0-dev+185-gSHA"}'}, + ) + assert result.exit_code == 0, result.output + settings = settings_from_call(mock) + assert isinstance(settings.dev_spec, DevBuildSpec) + assert settings.dev_spec.version == "2026.05.0-dev+185-gSHA" + assert settings.dev_spec.channel is None + + def test_dev_spec_absent_is_none(self): + """When --dev-spec is absent, BakerySettings.dev_spec is None.""" + result, mock = self._invoke([]) + assert result.exit_code == 0, result.output + settings = settings_from_call(mock) + assert settings.dev_spec is None + + def test_dev_spec_invalid_json_rejected(self): + """Invalid JSON in --dev-spec causes a non-zero exit.""" + result, _ = self._invoke(["--dev-spec", "not-json"]) + assert result.exit_code != 0 + + def test_dev_spec_invalid_schema_rejected(self): + """JSON with unknown fields is rejected (extra='forbid' on DevBuildSpec).""" + result, _ = self._invoke(["--dev-spec", '{"version": "1.0.0", "chanenl": "daily"}']) + assert result.exit_code != 0 + + +class TestCiPublishDevSpec: + """Tests for --dev-spec / BAKERY_DEV_SPEC in bakery ci publish.""" + + def _invoke(self, extra_args: list[str], env: dict | None = None): + with ( + patch("posit_bakery.cli.ci.BakeryConfig") as mock_config, + patch("posit_bakery.plugins.builtin.oras.oras.find_oras_bin", return_value=Path("/fake/oras")), + patch("posit_bakery.plugins.registry.get_plugin") as mock_get_plugin, + ): + instance = MagicMock() + instance.load_build_metadata_from_file.return_value = [] + instance.base_path = Path(BASIC_CONTEXT) + mock_config.from_context.return_value = instance + mock_soci = MagicMock() + mock_soci.execute.return_value = [] + mock_get_plugin.return_value = mock_soci + result = runner.invoke( + app, + ["ci", "publish", "/fake/metadata.json", "--context", BASIC_CONTEXT] + extra_args, + env=env, + catch_exceptions=False, + ) + return result, mock_config + + def test_dev_spec_via_flag(self): + """--dev-spec JSON is parsed and forwarded to BakerySettings in ci publish.""" + result, mock = self._invoke(["--dev-spec", '{"version": "2026.05.0-dev+185-gSHA", "channel": "daily"}']) + assert result.exit_code == 0, result.output + settings = settings_from_call(mock) + assert isinstance(settings.dev_spec, DevBuildSpec) + assert settings.dev_spec.version == "2026.05.0-dev+185-gSHA" + assert settings.dev_spec.channel == ReleaseChannelEnum.DAILY + + def test_dev_spec_via_env_var(self): + """BAKERY_DEV_SPEC env var works in ci publish.""" + result, mock = self._invoke( + [], + env={"BAKERY_DEV_SPEC": '{"version": "2026.05.0-dev+185-gSHA"}'}, + ) + assert result.exit_code == 0, result.output + settings = settings_from_call(mock) + assert isinstance(settings.dev_spec, DevBuildSpec) + assert settings.dev_spec.version == "2026.05.0-dev+185-gSHA" + assert settings.dev_spec.channel is None + + def test_dev_spec_absent_is_none(self): + """When --dev-spec is absent, BakerySettings.dev_spec is None.""" + result, mock = self._invoke([]) + assert result.exit_code == 0, result.output + settings = settings_from_call(mock) + assert settings.dev_spec is None + + def test_dev_spec_invalid_json_rejected(self): + """Invalid JSON in --dev-spec causes a non-zero exit.""" + result, _ = self._invoke(["--dev-spec", "not-json"]) + assert result.exit_code != 0 + + def test_dev_spec_invalid_schema_rejected(self): + """JSON with unknown fields is rejected (extra='forbid' on DevBuildSpec).""" + result, _ = self._invoke(["--dev-spec", '{"version": "1.0.0", "chanenl": "daily"}']) + assert result.exit_code != 0