diff --git a/.github/workflows/bakery-build-native.yml b/.github/workflows/bakery-build-native.yml index 0a9d9f54..73d57d08 100644 --- a/.github/workflows/bakery-build-native.yml +++ b/.github/workflows/bakery-build-native.yml @@ -27,6 +27,21 @@ on: default: "exclude" required: false type: string + image-version: + description: "Filter to a specific image version (e.g. product version from upstream dispatch)" + default: "" + required: false + type: string + dev-stream: + description: "Filter dev versions to a specific release stream (e.g. 'daily', 'preview')" + default: "" + required: false + type: string + dev-channel: + description: "Upstream channel reference (e.g. branch codename for Workbench preview)" + default: "" + required: false + type: string push: description: "Whether to push images to registries [default: false]" default: false @@ -105,17 +120,31 @@ jobs: env: DEV_VERSIONS: ${{ inputs.dev-versions }} MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + IMAGE_VERSION: ${{ inputs.image-version }} CONTEXT: ${{ inputs.context }} + DEV_STREAM: ${{ inputs.dev-stream }} + DEV_CHANNEL: ${{ inputs.dev-channel }} run: | - echo "platform_matrix=$(bakery ci matrix --quiet --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS" --context "$CONTEXT" | jq --compact-output .)" >> $GITHUB_OUTPUT + ARGS=(--quiet --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS" --context "$CONTEXT") + [[ -n "$IMAGE_VERSION" ]] && ARGS+=(--image-version "$IMAGE_VERSION") + [[ -n "$DEV_STREAM" ]] && ARGS+=(--dev-stream "$DEV_STREAM") + [[ -n "$DEV_CHANNEL" ]] && ARGS+=(--value "channel=$DEV_CHANNEL") + echo "platform_matrix=$(bakery ci matrix "${ARGS[@]}" | jq --compact-output .)" >> $GITHUB_OUTPUT - name: Images by Version id: images-by-version env: DEV_VERSIONS: ${{ inputs.dev-versions }} MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + IMAGE_VERSION: ${{ inputs.image-version }} CONTEXT: ${{ inputs.context }} + DEV_STREAM: ${{ inputs.dev-stream }} + DEV_CHANNEL: ${{ inputs.dev-channel }} run: | - echo "versions_matrix=$(bakery ci matrix --quiet --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS" --exclude platform --context "$CONTEXT" | jq --compact-output .)" >> $GITHUB_OUTPUT + ARGS=(--quiet --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS" --exclude platform --context "$CONTEXT") + [[ -n "$IMAGE_VERSION" ]] && ARGS+=(--image-version "$IMAGE_VERSION") + [[ -n "$DEV_STREAM" ]] && ARGS+=(--dev-stream "$DEV_STREAM") + [[ -n "$DEV_CHANNEL" ]] && ARGS+=(--value "channel=$DEV_CHANNEL") + echo "versions_matrix=$(bakery ci matrix "${ARGS[@]}" | jq --compact-output .)" >> $GITHUB_OUTPUT build-test: name: "Build/Test ${{ matrix.img.image }}:${{ matrix.img.version }} (${{ matrix.img.platform }})" @@ -209,24 +238,22 @@ jobs: IMAGE_PLATFORM: ${{ matrix.img.platform }} DEV_VERSIONS: ${{ inputs.dev-versions }} MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + DEV_STREAM: ${{ inputs.dev-stream }} + DEV_CHANNEL: ${{ inputs.dev-channel }} REGISTRY: ghcr.io/${{ github.repository_owner }} NORMALIZED_PLATFORM: ${{ steps.normalize-platform.outputs.platform }} CONTEXT: ${{ inputs.context }} # Cache-to is conditional on --push (handled by bakery internally) run: | - bakery build \ - --strategy build --pull \ - --retry "$RETRY" \ - --image-name "^${IMAGE_NAME}$" \ - --image-version "$IMAGE_VERSION" \ - --image-platform "$IMAGE_PLATFORM" \ - --dev-versions "$DEV_VERSIONS" \ - --matrix-versions "$MATRIX_VERSIONS" \ - --cache-registry "$REGISTRY" \ - --temp-registry "$REGISTRY" \ - --metadata-file "./${IMAGE_NAME}-${IMAGE_VERSION}-${NORMALIZED_PLATFORM}-metadata.json" \ - --context "$CONTEXT" \ - --push + ARGS=(--strategy build --pull --retry "$RETRY") + ARGS+=(--image-name "^${IMAGE_NAME}$" --image-version "$IMAGE_VERSION" --image-platform "$IMAGE_PLATFORM") + ARGS+=(--dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS") + ARGS+=(--cache-registry "$REGISTRY" --temp-registry "$REGISTRY") + ARGS+=(--metadata-file "./${IMAGE_NAME}-${IMAGE_VERSION}-${NORMALIZED_PLATFORM}-metadata.json") + ARGS+=(--context "$CONTEXT" --push) + [[ -n "$DEV_STREAM" ]] && ARGS+=(--dev-stream "$DEV_STREAM") + [[ -n "$DEV_CHANNEL" ]] && ARGS+=(--value "channel=$DEV_CHANNEL") + bakery build "${ARGS[@]}" - name: Test env: IMAGE_NAME: ${{ matrix.img.image }} @@ -234,19 +261,20 @@ jobs: IMAGE_PLATFORM: ${{ matrix.img.platform }} DEV_VERSIONS: ${{ inputs.dev-versions }} MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + DEV_STREAM: ${{ inputs.dev-stream }} + DEV_CHANNEL: ${{ inputs.dev-channel }} NORMALIZED_PLATFORM: ${{ steps.normalize-platform.outputs.platform }} CONTEXT: ${{ inputs.context }} run: | + ARGS=(--image-name "^${IMAGE_NAME}$" --image-version "$IMAGE_VERSION" --image-platform "$IMAGE_PLATFORM") + ARGS+=(--dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS") + ARGS+=(--metadata-file "./${IMAGE_NAME}-${IMAGE_VERSION}-${NORMALIZED_PLATFORM}-metadata.json") + ARGS+=(--context "$CONTEXT") + [[ -n "$DEV_STREAM" ]] && ARGS+=(--dev-stream "$DEV_STREAM") + [[ -n "$DEV_CHANNEL" ]] && ARGS+=(--value "channel=$DEV_CHANNEL") GOSS_PATH=${GITHUB_WORKSPACE}/tools/goss \ DGOSS_PATH=${GITHUB_WORKSPACE}/tools/dgoss \ - bakery run dgoss \ - --image-name "^${IMAGE_NAME}$" \ - --image-version "$IMAGE_VERSION" \ - --image-platform "$IMAGE_PLATFORM" \ - --dev-versions "$DEV_VERSIONS" \ - --matrix-versions "$MATRIX_VERSIONS" \ - --metadata-file "./${IMAGE_NAME}-${IMAGE_VERSION}-${NORMALIZED_PLATFORM}-metadata.json" \ - --context "$CONTEXT" + bakery run dgoss "${ARGS[@]}" - name: Upload Metadata uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -346,13 +374,12 @@ jobs: CONTEXT: ${{ inputs.context }} REGISTRY: ghcr.io/${{ github.repository_owner }} PUSH: ${{ inputs.push }} + DEV_CHANNEL: ${{ inputs.dev-channel }} run: | - if [ "$PUSH" = "true" ]; then PUSH_FLAG=""; else PUSH_FLAG="--dry-run"; fi - bakery ci merge \ - --context "$CONTEXT" \ - --temp-registry "$REGISTRY" \ - $PUSH_FLAG \ - *-metadata.json + ARGS=(--context "$CONTEXT" --temp-registry "$REGISTRY") + [[ -n "$DEV_CHANNEL" ]] && ARGS+=(--value "channel=$DEV_CHANNEL") + if [ "$PUSH" != "true" ]; then ARGS+=(--dry-run); fi + bakery ci merge "${ARGS[@]}" *-metadata.json readme: name: Push READMEs @@ -379,8 +406,8 @@ jobs: CONTEXT: ${{ inputs.context }} DEV_VERSIONS: ${{ inputs.dev-versions }} MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + DEV_CHANNEL: ${{ inputs.dev-channel }} run: | - bakery ci readme \ - --context "$CONTEXT" \ - --dev-versions "$DEV_VERSIONS" \ - --matrix-versions "$MATRIX_VERSIONS" + ARGS=(--context "$CONTEXT" --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS") + [[ -n "$DEV_CHANNEL" ]] && ARGS+=(--value "channel=$DEV_CHANNEL") + bakery ci readme "${ARGS[@]}" diff --git a/.github/workflows/bakery-build.yml b/.github/workflows/bakery-build.yml index 9f33329a..fc81cb59 100644 --- a/.github/workflows/bakery-build.yml +++ b/.github/workflows/bakery-build.yml @@ -28,6 +28,21 @@ on: default: "exclude" required: false type: string + image-version: + description: "Filter to a specific image version (e.g. product version from upstream dispatch)" + default: "" + required: false + type: string + dev-stream: + description: "Filter dev versions to a specific release stream (e.g. 'daily', 'preview')" + default: "" + required: false + type: string + dev-channel: + description: "Upstream channel reference (e.g. branch codename for Workbench preview)" + default: "" + required: false + type: string push: description: "Whether to push images to registries [default: false]" default: false @@ -95,9 +110,16 @@ jobs: env: DEV_VERSIONS: ${{ inputs.dev-versions }} MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + IMAGE_VERSION: ${{ inputs.image-version }} CONTEXT: ${{ inputs.context }} + DEV_STREAM: ${{ inputs.dev-stream }} + DEV_CHANNEL: ${{ inputs.dev-channel }} run: | - echo "matrix=$(bakery ci matrix --quiet --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS" --context "$CONTEXT" | jq --compact-output .)" >> $GITHUB_OUTPUT + ARGS=(--quiet --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS" --context "$CONTEXT") + [[ -n "$IMAGE_VERSION" ]] && ARGS+=(--image-version "$IMAGE_VERSION") + [[ -n "$DEV_STREAM" ]] && ARGS+=(--dev-stream "$DEV_STREAM") + [[ -n "$DEV_CHANNEL" ]] && ARGS+=(--value "channel=$DEV_CHANNEL") + echo "matrix=$(bakery ci matrix "${ARGS[@]}" | jq --compact-output .)" >> $GITHUB_OUTPUT build: name: "${{ matrix.img.image }}:${{ matrix.img.version }}" @@ -174,17 +196,18 @@ jobs: IMAGE_VERSION: ${{ matrix.img.version }} DEV_VERSIONS: ${{ inputs.dev-versions }} MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + DEV_STREAM: ${{ inputs.dev-stream }} + DEV_CHANNEL: ${{ inputs.dev-channel }} REGISTRY: ghcr.io/${{ github.repository_owner }} CONTEXT: ${{ inputs.context }} run: | - bakery build --load --pull \ - --retry "$RETRY" \ - --image-name "^${IMAGE_NAME}$" \ - --image-version "$IMAGE_VERSION" \ - --dev-versions "$DEV_VERSIONS" \ - --matrix-versions "$MATRIX_VERSIONS" \ - --cache-registry "$REGISTRY" \ - --context "$CONTEXT" + ARGS=(--load --pull --retry "$RETRY") + ARGS+=(--image-name "^${IMAGE_NAME}$" --image-version "$IMAGE_VERSION") + ARGS+=(--dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS") + ARGS+=(--cache-registry "$REGISTRY" --context "$CONTEXT") + [[ -n "$DEV_STREAM" ]] && ARGS+=(--dev-stream "$DEV_STREAM") + [[ -n "$DEV_CHANNEL" ]] && ARGS+=(--value "channel=$DEV_CHANNEL") + bakery build "${ARGS[@]}" - name: Test env: @@ -192,16 +215,18 @@ jobs: IMAGE_VERSION: ${{ matrix.img.version }} DEV_VERSIONS: ${{ inputs.dev-versions }} MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + DEV_STREAM: ${{ inputs.dev-stream }} + DEV_CHANNEL: ${{ inputs.dev-channel }} CONTEXT: ${{ inputs.context }} run: | + ARGS=(--image-name "^${IMAGE_NAME}$" --image-version "$IMAGE_VERSION") + ARGS+=(--dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS") + ARGS+=(--context "$CONTEXT") + [[ -n "$DEV_STREAM" ]] && ARGS+=(--dev-stream "$DEV_STREAM") + [[ -n "$DEV_CHANNEL" ]] && ARGS+=(--value "channel=$DEV_CHANNEL") GOSS_PATH=${GITHUB_WORKSPACE}/tools/goss \ DGOSS_PATH=${GITHUB_WORKSPACE}/tools/dgoss \ - bakery run dgoss \ - --image-name "^${IMAGE_NAME}$" \ - --image-version "$IMAGE_VERSION" \ - --dev-versions "$DEV_VERSIONS" \ - --matrix-versions "$MATRIX_VERSIONS" \ - --context "$CONTEXT" + bakery run dgoss "${ARGS[@]}" - name: Push # Since this is a reusable workflow, we need to be very explicit about @@ -215,15 +240,17 @@ jobs: IMAGE_VERSION: ${{ matrix.img.version }} DEV_VERSIONS: ${{ inputs.dev-versions }} MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + DEV_STREAM: ${{ inputs.dev-stream }} + DEV_CHANNEL: ${{ inputs.dev-channel }} CONTEXT: ${{ inputs.context }} run: | - bakery build --push --pull \ - --retry "$RETRY" \ - --image-name "^${IMAGE_NAME}$" \ - --image-version "$IMAGE_VERSION" \ - --dev-versions "$DEV_VERSIONS" \ - --matrix-versions "$MATRIX_VERSIONS" \ - --context "$CONTEXT" + ARGS=(--push --pull --retry "$RETRY") + ARGS+=(--image-name "^${IMAGE_NAME}$" --image-version "$IMAGE_VERSION") + ARGS+=(--dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS") + ARGS+=(--context "$CONTEXT") + [[ -n "$DEV_STREAM" ]] && ARGS+=(--dev-stream "$DEV_STREAM") + [[ -n "$DEV_CHANNEL" ]] && ARGS+=(--value "channel=$DEV_CHANNEL") + bakery build "${ARGS[@]}" readme: name: Push READMEs @@ -250,8 +277,8 @@ jobs: CONTEXT: ${{ inputs.context }} DEV_VERSIONS: ${{ inputs.dev-versions }} MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + DEV_CHANNEL: ${{ inputs.dev-channel }} run: | - bakery ci readme \ - --context "$CONTEXT" \ - --dev-versions "$DEV_VERSIONS" \ - --matrix-versions "$MATRIX_VERSIONS" + ARGS=(--context "$CONTEXT" --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS") + [[ -n "$DEV_CHANNEL" ]] && ARGS+=(--value "channel=$DEV_CHANNEL") + bakery ci readme "${ARGS[@]}" diff --git a/posit-bakery/docs/cross-repo-workflows.qmd b/posit-bakery/docs/cross-repo-workflows.qmd index a2e1ec1e..07a07a02 100644 --- a/posit-bakery/docs/cross-repo-workflows.qmd +++ b/posit-bakery/docs/cross-repo-workflows.qmd @@ -56,53 +56,44 @@ posit-platform is a future option once the per-product chains are stable. %%| fig-cap: "Production Release Flow" graph TD subgraph "Product Repos" - CONNECT_PROD["posit-dev/connect"] - WORKBENCH_PROD["rstudio/rstudio-pro"] - PPM_PROD["rstudio/package-manager"] + CONNECT_PROD["connect"] + WORKBENCH_PROD["rstudio-pro"] + PPM_PROD["package-manager"] end - CONNECT_BOT["posit-connect-projects"] - WORKBENCH_BOT["workbench-ide-release"] - PPM_BOT["posit-ppm"] - - CONNECT_PROD -.-> CONNECT_BOT - WORKBENCH_PROD -.-> WORKBENCH_BOT - PPM_PROD -.-> PPM_BOT - - CONNECT_BOT -.->|"workflow_dispatch release.yml
(version)"| IMG_CONNECT - WORKBENCH_BOT -.->|"workflow_dispatch release.yml
(version)"| IMG_WORKBENCH - PPM_BOT -.->|"workflow_dispatch release.yml
(version)"| IMG_PM - - SHARED["posit-dev/images-shared
bakery-build-native
bakery-build
product-release
clean"] + CONNECT_PROD -.->|"dispatch release.yml
version"| IMG_CONNECT + WORKBENCH_PROD -.->|"dispatch release.yml
version"| IMG_WORKBENCH + PPM_PROD -.->|"dispatch release.yml
version"| IMG_PM subgraph "Image Repos" - IMG_CONNECT["posit-dev/images-connect
production
content
release"] - IMG_WORKBENCH["posit-dev/images-workbench
production
session
release"] - IMG_PM["posit-dev/images-package-manager
production
release"] + IMG_CONNECT["images-connect"] + IMG_WORKBENCH["images-workbench"] + IMG_PM["images-package-manager"] end - IMG_CONNECT -.->|workflow_call| SHARED - IMG_WORKBENCH -.->|workflow_call| SHARED - IMG_PM -.->|workflow_call| SHARED + IMG_CONNECT -.->|"workflow_call
bakery-build-native.yml"| SHARED + IMG_WORKBENCH -.->|"workflow_call
bakery-build-native.yml"| SHARED + IMG_PM -.->|"workflow_call
bakery-build-native.yml"| SHARED - IMG_CONNECT -->|push| DOCKERHUB - IMG_CONNECT -->|push| GHCR - IMG_WORKBENCH -->|push| DOCKERHUB - IMG_WORKBENCH -->|push| GHCR - IMG_PM -->|push| DOCKERHUB - IMG_PM -->|push| GHCR + subgraph images-shared + SHARED["bakery-build-native.yml
product-release.yml"] + end + + IMG_CONNECT -->|push| REGISTRIES + IMG_WORKBENCH -->|push| REGISTRIES + IMG_PM -->|push| REGISTRIES - DOCKERHUB["Docker Hub"] - GHCR["GHCR"] + REGISTRIES["Docker Hub + GHCR"] - IMG_CONNECT -.->|"workflow_dispatch product-release.yml"| HELM - IMG_WORKBENCH -.->|"workflow_dispatch product-release.yml"| HELM - IMG_PM -.->|"workflow_dispatch product-release.yml"| HELM + IMG_CONNECT -.->|"dispatch product-release.yml
product, version"| HELM_WF + IMG_WORKBENCH -.->|"dispatch product-release.yml
product, version"| HELM_WF + IMG_PM -.->|"dispatch product-release.yml
product, version"| HELM_WF - HELM["rstudio/helm
product-release
chart-releaser"] - HELM -->|Flux sync| K8S + subgraph helm + HELM_WF["product-release.yml"] + end - K8S["K8s Dogfood Sites"] + HELM_WF -->|Flux sync| K8S["K8s Dogfood Sites"] ``` ## Development / Preview Flow @@ -111,32 +102,28 @@ graph TD %%| fig-cap: "Development / Preview Flow" graph TD subgraph "Product Repos" - CONNECT_PROD["posit-dev/connect"] - WORKBENCH_PROD["rstudio/rstudio-pro"] - PPM_PROD["rstudio/package-manager"] + CONNECT_PROD["connect"] + WORKBENCH_PROD["rstudio-pro"] + PPM_PROD["package-manager"] end - CONNECT_BOT["posit-connect-projects"] - WORKBENCH_BOT["workbench-ide-release"] - PPM_BOT["posit-ppm"] + CONNECT_PROD -.->|"dispatch development.yml
version"| IMG_CONNECT + WORKBENCH_PROD -.->|"dispatch development.yml
version, stream, channel"| IMG_WORKBENCH + PPM_PROD -.->|"dispatch development.yml
version, stream"| IMG_PM - CONNECT_PROD -.-> CONNECT_BOT - WORKBENCH_PROD -.-> WORKBENCH_BOT - PPM_PROD -.-> PPM_BOT - - CONNECT_BOT -.->|"workflow_dispatch development.yml
(version)"| IMG_CONNECT - WORKBENCH_BOT -.->|"workflow_dispatch development.yml
(version)"| IMG_WORKBENCH - PPM_BOT -.->|"workflow_dispatch development.yml
(version)"| IMG_PM - - SHARED["posit-dev/images-shared
bakery-build-native
bakery-build"] + subgraph "Image Repos" + IMG_CONNECT["images-connect
development.yml"] + IMG_WORKBENCH["images-workbench
development.yml"] + IMG_PM["images-package-manager
development.yml"] + end - IMG_CONNECT["posit-dev/images-connect
development"] - IMG_WORKBENCH["posit-dev/images-workbench
development"] - IMG_PM["posit-dev/images-package-manager
development"] + IMG_CONNECT -.->|"workflow_call
dev-versions, image-version,
dev-stream"| SHARED + IMG_WORKBENCH -.->|"workflow_call
dev-versions, image-version,
dev-stream, dev-channel"| SHARED + IMG_PM -.->|"workflow_call
dev-versions, image-version,
dev-stream"| SHARED - IMG_CONNECT -.->|workflow_call| SHARED - IMG_WORKBENCH -.->|workflow_call| SHARED - IMG_PM -.->|workflow_call| SHARED + subgraph images-shared + SHARED["bakery-build-native.yml"] + end IMG_CONNECT -->|preview push| GHCR IMG_WORKBENCH -->|preview push| GHCR @@ -144,13 +131,9 @@ graph TD GHCR["GHCR
connect-preview
workbench-preview
workbench-session-init-preview
package-manager-preview"] - GHCR --> K8S - GHCR --> FUZZBUCKET - GHCR --> EKS_REF - - K8S["K8s Dogfood Sites"] - FUZZBUCKET["Fuzzbucket
IDE Automation"] - EKS_REF["EKS Reference Architecture"] + GHCR --> K8S["K8s Dogfood Sites"] + GHCR --> FUZZBUCKET["Fuzzbucket
IDE Automation"] + GHCR --> EKS_REF["EKS Reference Architecture"] ``` ## Per-Product Diagrams @@ -162,38 +145,53 @@ graph TD ```{mermaid} %%| fig-cap: "Connect Production Flow" graph TD - PROD["posit-dev/connect
release-scripts.yml"] - BOT["posit-connect-projects"] + subgraph connect + SCRIPTS["release-scripts.yml
publish_release.py"] + end + + SCRIPTS -.->|"workflow_dispatch
version"| REL - PROD -.-> BOT - BOT -.->|"workflow_dispatch release.yml
(version)"| IMG_RELEASE + subgraph images-connect + REL["release.yml"] + PROD_WF["production.yml"] + CONTENT["content.yml"] + HELM_JOB["update-helm job"] + end - SHARED["posit-dev/images-shared
bakery-build-native
product-release"] + REL -.->|"workflow_call
version, images"| PRODUCT_REL - IMG_RELEASE["posit-dev/images-connect
release"] - IMG_PROD["posit-dev/images-connect
production"] - IMG_CONTENT["posit-dev/images-connect
content"] + subgraph images-shared + PRODUCT_REL["product-release.yml"] + SHARED["bakery-build-native.yml"] + end - IMG_RELEASE -.->|workflow_call| SHARED - IMG_PROD -.->|workflow_call| SHARED - IMG_CONTENT -.->|workflow_call| SHARED + REL -->|"PR merge triggers"| PROD_WF + REL -->|"PR merge triggers"| CONTENT - IMG_RELEASE -->|"merge to main"| IMG_PROD - IMG_RELEASE -->|"merge to main"| IMG_CONTENT + PROD_WF -.->|"workflow_call
dev-versions=exclude"| SHARED + CONTENT -.->|"workflow_call
matrix-versions=only"| SHARED - IMG_PROD -->|push| DOCKERHUB - IMG_PROD -->|push| GHCR - IMG_CONTENT -->|push| DOCKERHUB - IMG_CONTENT -->|push| GHCR + PROD_WF --> HELM_JOB + HELM_JOB -.->|"workflow_dispatch
product=connect, version"| HELM_WF - DOCKERHUB["Docker Hub
rstudio/rstudio-connect
rstudio/rstudio-connect-content-init"] - GHCR["GHCR"] + PROD_WF -->|push| DOCKERHUB["Docker Hub"] + PROD_WF -->|push| GHCR["GHCR"] + CONTENT -->|push| DOCKERHUB + CONTENT -->|push| GHCR - IMG_PROD -.->|"workflow_dispatch product-release.yml"| HELM - HELM["rstudio/helm
product-release
chart-releaser"] - HELM -->|Flux sync| K8S + subgraph helm + HELM_WF["product-release.yml"] + end - K8S["K8s Dogfood Sites"] + HELM_WF -->|Flux sync| K8S["K8s Dogfood Sites"] + + click SCRIPTS "https://github.com/posit-dev/connect/blob/main/.github/workflows/release-scripts.yml" _blank + click REL "https://github.com/posit-dev/images-connect/blob/main/.github/workflows/release.yml" _blank + click PROD_WF "https://github.com/posit-dev/images-connect/blob/main/.github/workflows/production.yml" _blank + click CONTENT "https://github.com/posit-dev/images-connect/blob/main/.github/workflows/content.yml" _blank + click PRODUCT_REL "https://github.com/posit-dev/images-shared/blob/main/.github/workflows/product-release.yml" _blank + click SHARED "https://github.com/posit-dev/images-shared/blob/main/.github/workflows/bakery-build-native.yml" _blank + click HELM_WF "https://github.com/rstudio/helm/blob/main/.github/workflows/product-release.yml" _blank ``` #### Development {.unnumbered} @@ -201,25 +199,28 @@ graph TD ```{mermaid} %%| fig-cap: "Connect Development Flow" graph TD - PROD["posit-dev/connect
ci.yml (push to main)"] - BOT["posit-connect-projects"] - - PROD -.-> BOT - BOT -.->|"workflow_dispatch development.yml
(version)"| IMG_DEV - - SHARED["posit-dev/images-shared
bakery-build-native"] + subgraph connect + CI["ci.yml"] + end - IMG_DEV["posit-dev/images-connect
development"] + CI -.->|"workflow_dispatch
version"| DEV - IMG_DEV -.->|workflow_call| SHARED + subgraph images-connect + DEV["development.yml"] + end - IMG_DEV -->|preview push| GHCR + DEV -.->|"workflow_call
dev-versions, image-version,
dev-stream"| SHARED - GHCR["GHCR
connect-preview"] + subgraph images-shared + SHARED["bakery-build-native.yml"] + end - GHCR --> K8S + DEV -->|preview push| GHCR["GHCR
connect-preview"] + GHCR --> K8S["K8s Dogfood Sites"] - K8S["K8s Dogfood Sites"] + click CI "https://github.com/posit-dev/connect/blob/main/.github/workflows/ci.yml" _blank + click DEV "https://github.com/posit-dev/images-connect/blob/main/.github/workflows/development.yml" _blank + click SHARED "https://github.com/posit-dev/images-shared/blob/main/.github/workflows/bakery-build-native.yml" _blank ``` ### Workbench @@ -229,38 +230,56 @@ graph TD ```{mermaid} %%| fig-cap: "Workbench Production Flow" graph TD - PROD["rstudio/rstudio-pro
release-all.yml"] - BOT["workbench-ide-release"] + subgraph rstudio-pro + RELEASE_ALL["release-all.yml"] + UPDATE_IMG["release-update-images-workbench.yml"] + RELEASE_ALL --> UPDATE_IMG + end + + UPDATE_IMG -.->|"workflow_dispatch
version"| REL - PROD -.-> BOT - BOT -.->|"workflow_dispatch release.yml
(version)"| IMG_RELEASE + subgraph images-workbench + REL["release.yml"] + PROD_WF["production.yml"] + SESSION["session.yml"] + HELM_JOB["update-helm job"] + end - SHARED["posit-dev/images-shared
bakery-build-native
product-release"] + REL -.->|"workflow_call
version, images"| PRODUCT_REL - IMG_RELEASE["posit-dev/images-workbench
release"] - IMG_PROD["posit-dev/images-workbench
production"] - IMG_SESSION["posit-dev/images-workbench
session"] + subgraph images-shared + PRODUCT_REL["product-release.yml"] + SHARED["bakery-build-native.yml"] + end - IMG_RELEASE -.->|workflow_call| SHARED - IMG_PROD -.->|workflow_call| SHARED - IMG_SESSION -.->|workflow_call| SHARED + REL -->|"PR merge triggers"| PROD_WF + REL -->|"PR merge triggers"| SESSION - IMG_RELEASE -->|"merge to main"| IMG_PROD - IMG_RELEASE -->|"merge to main"| IMG_SESSION + PROD_WF -.->|"workflow_call
dev-versions=exclude"| SHARED + SESSION -.->|"workflow_call
matrix-versions=only"| SHARED - IMG_PROD -->|push| DOCKERHUB - IMG_PROD -->|push| GHCR - IMG_SESSION -->|push| DOCKERHUB - IMG_SESSION -->|push| GHCR + PROD_WF --> HELM_JOB + HELM_JOB -.->|"workflow_dispatch
product=workbench, version"| HELM_WF - DOCKERHUB["Docker Hub
rstudio/rstudio-workbench
rstudio/r-session-complete"] - GHCR["GHCR"] + PROD_WF -->|push| DOCKERHUB["Docker Hub"] + PROD_WF -->|push| GHCR["GHCR"] + SESSION -->|push| DOCKERHUB + SESSION -->|push| GHCR - IMG_PROD -.->|"workflow_dispatch product-release.yml"| HELM - HELM["rstudio/helm
product-release
chart-releaser"] - HELM -->|Flux sync| K8S + subgraph helm + HELM_WF["product-release.yml"] + end - K8S["K8s Dogfood Sites"] + HELM_WF -->|Flux sync| K8S["K8s Dogfood Sites"] + + click RELEASE_ALL "https://github.com/rstudio/rstudio-pro/blob/main/.github/workflows/release-all.yml" _blank + click UPDATE_IMG "https://github.com/rstudio/rstudio-pro/blob/main/.github/workflows/release-update-images-workbench.yml" _blank + click REL "https://github.com/posit-dev/images-workbench/blob/main/.github/workflows/release.yml" _blank + click PROD_WF "https://github.com/posit-dev/images-workbench/blob/main/.github/workflows/production.yml" _blank + click SESSION "https://github.com/posit-dev/images-workbench/blob/main/.github/workflows/session.yml" _blank + click PRODUCT_REL "https://github.com/posit-dev/images-shared/blob/main/.github/workflows/product-release.yml" _blank + click SHARED "https://github.com/posit-dev/images-shared/blob/main/.github/workflows/bakery-build-native.yml" _blank + click HELM_WF "https://github.com/rstudio/helm/blob/main/.github/workflows/product-release.yml" _blank ``` #### Development {.unnumbered} @@ -268,29 +287,30 @@ graph TD ```{mermaid} %%| fig-cap: "Workbench Development Flow" graph TD - PROD["rstudio/rstudio-pro
release-nightly-test.yml"] - BOT["workbench-ide-release"] - - PROD -.-> BOT - BOT -.->|"workflow_dispatch development.yml
(version)"| IMG_DEV - - SHARED["posit-dev/images-shared
bakery-build-native"] + subgraph rstudio-pro + NIGHTLY["release-nightly-test.yml"] + end - IMG_DEV["posit-dev/images-workbench
development"] + NIGHTLY -.->|"workflow_dispatch
version, stream, channel"| DEV - IMG_DEV -.->|workflow_call| SHARED + subgraph images-workbench + DEV["development.yml"] + end - IMG_DEV -->|preview push| GHCR + DEV -.->|"workflow_call
dev-versions, image-version,
dev-stream, dev-channel"| SHARED - GHCR["GHCR
workbench-preview
workbench-session-init-preview"] + subgraph images-shared + SHARED["bakery-build-native.yml"] + end - GHCR --> K8S - GHCR --> FUZZBUCKET - GHCR --> EKS_REF + DEV -->|preview push| GHCR["GHCR
workbench-preview
workbench-session-init-preview"] + GHCR --> K8S["K8s Dogfood Sites"] + GHCR --> FUZZBUCKET["Fuzzbucket
IDE Automation"] + GHCR --> EKS_REF["EKS Reference Architecture"] - K8S["K8s Dogfood Sites"] - FUZZBUCKET["Fuzzbucket
IDE Automation"] - EKS_REF["EKS Reference Architecture"] + click NIGHTLY "https://github.com/rstudio/rstudio-pro/blob/main/.github/workflows/release-nightly-test.yml" _blank + click DEV "https://github.com/posit-dev/images-workbench/blob/main/.github/workflows/development.yml" _blank + click SHARED "https://github.com/posit-dev/images-shared/blob/main/.github/workflows/bakery-build-native.yml" _blank ``` ### Package Manager @@ -300,33 +320,46 @@ graph TD ```{mermaid} %%| fig-cap: "Package Manager Production Flow" graph TD - PROD["rstudio/package-manager
ci.yml (tag push)"] - BOT["posit-ppm"] + subgraph package-manager + CI["ci.yml (publish job)"] + end + + CI -.->|"workflow_dispatch
version"| REL - PROD -.-> BOT - BOT -.->|"workflow_dispatch release.yml
(version)"| IMG_RELEASE + subgraph images-package-manager + REL["release.yml"] + PROD_WF["production.yml"] + HELM_JOB["update-helm job"] + end - SHARED["posit-dev/images-shared
bakery-build-native
product-release"] + REL -.->|"workflow_call
version, images"| PRODUCT_REL - IMG_RELEASE["posit-dev/images-package-manager
release"] - IMG_PROD["posit-dev/images-package-manager
production"] + subgraph images-shared + PRODUCT_REL["product-release.yml"] + SHARED["bakery-build-native.yml"] + end - IMG_RELEASE -.->|workflow_call| SHARED - IMG_PROD -.->|workflow_call| SHARED + REL -->|"PR merge triggers"| PROD_WF + PROD_WF -.->|"workflow_call
dev-versions=exclude"| SHARED - IMG_RELEASE -->|"merge to main"| IMG_PROD + PROD_WF --> HELM_JOB + HELM_JOB -.->|"workflow_dispatch
product=package-manager, version"| HELM_WF - IMG_PROD -->|push| DOCKERHUB - IMG_PROD -->|push| GHCR + PROD_WF -->|push| DOCKERHUB["Docker Hub"] + PROD_WF -->|push| GHCR["GHCR"] - DOCKERHUB["Docker Hub
rstudio/rstudio-package-manager"] - GHCR["GHCR"] + subgraph helm + HELM_WF["product-release.yml"] + end - IMG_PROD -.->|"workflow_dispatch product-release.yml"| HELM - HELM["rstudio/helm
product-release
chart-releaser"] - HELM -->|Flux sync| K8S + HELM_WF -->|Flux sync| K8S["K8s Dogfood Sites"] - K8S["K8s Dogfood Sites"] + click CI "https://github.com/rstudio/package-manager/blob/main/.github/workflows/ci.yml" _blank + click REL "https://github.com/posit-dev/images-package-manager/blob/main/.github/workflows/release.yml" _blank + click PROD_WF "https://github.com/posit-dev/images-package-manager/blob/main/.github/workflows/production.yml" _blank + click PRODUCT_REL "https://github.com/posit-dev/images-shared/blob/main/.github/workflows/product-release.yml" _blank + click SHARED "https://github.com/posit-dev/images-shared/blob/main/.github/workflows/bakery-build-native.yml" _blank + click HELM_WF "https://github.com/rstudio/helm/blob/main/.github/workflows/product-release.yml" _blank ``` #### Development {.unnumbered} @@ -334,25 +367,28 @@ graph TD ```{mermaid} %%| fig-cap: "Package Manager Development Flow" graph TD - PROD["rstudio/package-manager
ci.yml (push to main)"] - BOT["posit-ppm"] - - PROD -.-> BOT - BOT -.->|"workflow_dispatch development.yml
(version)"| IMG_DEV - - SHARED["posit-dev/images-shared
bakery-build-native"] + subgraph package-manager + CI["ci.yml (publish job)"] + end - IMG_DEV["posit-dev/images-package-manager
development"] + CI -.->|"workflow_dispatch
version, stream"| DEV - IMG_DEV -.->|workflow_call| SHARED + subgraph images-package-manager + DEV["development.yml"] + end - IMG_DEV -->|preview push| GHCR + DEV -.->|"workflow_call
dev-versions, image-version,
dev-stream"| SHARED - GHCR["GHCR
package-manager-preview"] + subgraph images-shared + SHARED["bakery-build-native.yml"] + end - GHCR --> K8S + DEV -->|preview push| GHCR["GHCR
package-manager-preview"] + GHCR --> K8S["K8s Dogfood Sites"] - K8S["K8s Dogfood Sites"] + click CI "https://github.com/rstudio/package-manager/blob/main/.github/workflows/ci.yml" _blank + click DEV "https://github.com/posit-dev/images-package-manager/blob/main/.github/workflows/development.yml" _blank + click SHARED "https://github.com/posit-dev/images-shared/blob/main/.github/workflows/bakery-build-native.yml" _blank ``` ## Reference Tables diff --git a/posit-bakery/posit_bakery/cli/build.py b/posit-bakery/posit_bakery/cli/build.py index 9d3bc733..510936cf 100644 --- a/posit-bakery/posit_bakery/cli/build.py +++ b/posit-bakery/posit_bakery/cli/build.py @@ -7,7 +7,7 @@ import python_on_whales import typer -from posit_bakery.cli.common import with_verbosity_flags, with_temporary_storage +from posit_bakery.cli.common import with_verbosity_flags, with_temporary_storage, _make_value_map from posit_bakery.config import BakeryConfig from posit_bakery.config.config import BakeryConfigFilter, BakerySettings from posit_bakery.const import DevVersionInclusionEnum, MatrixVersionInclusionEnum @@ -186,6 +186,20 @@ def build( rich_help_panel=RichHelpPanelEnum.FILTERS, ), ] = MatrixVersionInclusionEnum.EXCLUDE, + dev_stream: Annotated[ + Optional[str], + typer.Option( + help="Filter development versions to a specific release stream (e.g. 'daily', 'preview').", + rich_help_panel=RichHelpPanelEnum.FILTERS, + ), + ] = None, + value: Annotated[ + Optional[list[str]], + typer.Option( + help="Override a devVersion value (key=value). Can be specified multiple times.", + rich_help_panel=RichHelpPanelEnum.FILTERS, + ), + ] = None, ) -> None: """Builds images in the context path @@ -196,6 +210,11 @@ def build( Requires Docker, Podman, or nerdctl to be installed and running for `--strategy build`. """ + value_map, errors = _make_value_map(value) + if errors: + for e in errors: + log.error(e) + raise typer.Exit(code=1) settings = BakerySettings( filter=BakeryConfigFilter( image_name=image_name, @@ -203,6 +222,8 @@ def build( image_variant=image_variant, image_os=image_os, image_platform=image_platform or [], + dev_stream=dev_stream, + values=value_map, ), dev_versions=dev_versions, matrix_versions=matrix_versions, diff --git a/posit-bakery/posit_bakery/cli/ci.py b/posit-bakery/posit_bakery/cli/ci.py index fe822cd9..9380bd5b 100644 --- a/posit-bakery/posit_bakery/cli/ci.py +++ b/posit-bakery/posit_bakery/cli/ci.py @@ -1,6 +1,7 @@ import glob import json import logging +import re import python_on_whales from enum import Enum from pathlib import Path @@ -8,7 +9,7 @@ import typer -from posit_bakery.cli.common import with_verbosity_flags +from posit_bakery.cli.common import with_verbosity_flags, _make_value_map from posit_bakery.config import BakeryConfig from posit_bakery.config.config import BakerySettings, BakeryConfigFilter from posit_bakery.const import DevVersionInclusionEnum, MatrixVersionInclusionEnum @@ -49,6 +50,28 @@ def matrix( rich_help_panel=RichHelpPanelEnum.FILTERS, ), ] = MatrixVersionInclusionEnum.EXCLUDE, + image_version: Annotated[ + Optional[str], + typer.Option( + show_default=False, + help="The image version to filter to.", + rich_help_panel=RichHelpPanelEnum.FILTERS, + ), + ] = None, + dev_stream: Annotated[ + Optional[str], + typer.Option( + help="Filter development versions to a specific release stream (e.g. 'daily', 'preview').", + rich_help_panel=RichHelpPanelEnum.FILTERS, + ), + ] = None, + value: Annotated[ + Optional[list[str]], + typer.Option( + help="Override a devVersion value (key=value). Can be specified multiple times.", + rich_help_panel=RichHelpPanelEnum.FILTERS, + ), + ] = None, exclude: Annotated[ Optional[list[BakeryCIMatrixFieldEnum]], typer.Option(help="Fields to exclude splitting the matrix by."), @@ -85,8 +108,18 @@ def matrix( exclude = [] try: + value_map, errors = _make_value_map(value) + if errors: + for e in errors: + log.error(e) + raise typer.Exit(code=1) settings = BakerySettings( - filter=BakeryConfigFilter(image_name=image_name), + filter=BakeryConfigFilter( + image_name=image_name, + image_version=re.escape(image_version) if image_version else None, + dev_stream=dev_stream, + values=value_map, + ), dev_versions=dev_versions, ) c = BakeryConfig.from_context(context=context, settings=settings) @@ -142,6 +175,12 @@ def merge( rich_help_panel="Build Configuration & Outputs", ), ] = None, + value: Annotated[ + Optional[list[str]], + typer.Option( + help="Override a devVersion value (key=value). Can be specified multiple times.", + ), + ] = None, dry_run: Annotated[ bool, typer.Option(help="If set, the merged images will not be pushed to the registry.") ] = False, @@ -162,7 +201,13 @@ def merge( } ``` """ + value_map, errors = _make_value_map(value) + if errors: + for e in errors: + log.error(e) + raise typer.Exit(code=1) settings = BakerySettings( + filter=BakeryConfigFilter(values=value_map), dev_versions=DevVersionInclusionEnum.INCLUDE, matrix_versions=MatrixVersionInclusionEnum.INCLUDE, clean_temporary=False, @@ -245,6 +290,12 @@ def readme( rich_help_panel=RichHelpPanelEnum.FILTERS, ), ] = MatrixVersionInclusionEnum.INCLUDE, + value: Annotated[ + Optional[list[str]], + typer.Option( + help="Override a devVersion value (key=value). Can be specified multiple times.", + ), + ] = None, ) -> None: """Push image READMEs to Docker Hub. @@ -256,7 +307,13 @@ def readme( variables to be set with a Personal Access Token (PAT). Organization Access Tokens cannot update repository descriptions. """ + value_map, errors = _make_value_map(value) + if errors: + for e in errors: + log.error(e) + raise typer.Exit(code=1) settings = BakerySettings( + filter=BakeryConfigFilter(values=value_map), dev_versions=dev_versions, matrix_versions=matrix_versions, ) diff --git a/posit-bakery/posit_bakery/cli/common.py b/posit-bakery/posit_bakery/cli/common.py index c930ff31..e40cdae7 100644 --- a/posit-bakery/posit_bakery/cli/common.py +++ b/posit-bakery/posit_bakery/cli/common.py @@ -90,7 +90,7 @@ def wrapper(ctx: typer.Context, *args, **kwargs) -> None: return wrapper -def __make_value_map(value: list[str] | None) -> tuple[dict[Any, Any], list[Exception]]: +def _make_value_map(value: list[str] | None) -> tuple[dict[Any, Any], list[Exception]]: """Parses key=value option pairs into a dictionary""" value_map = dict() errors = [] diff --git a/posit-bakery/posit_bakery/cli/create.py b/posit-bakery/posit_bakery/cli/create.py index c42a8da1..cecdd41b 100644 --- a/posit-bakery/posit_bakery/cli/create.py +++ b/posit-bakery/posit_bakery/cli/create.py @@ -7,7 +7,7 @@ from posit_bakery import error from posit_bakery.cli.common import ( - __make_value_map, + _make_value_map, with_verbosity_flags, __parse_dependency_constraint, __parse_dependency_versions, @@ -221,7 +221,7 @@ def version( └── Containerfile*.jinja2 ``` """ - value_map, errors = __make_value_map(value) + value_map, errors = _make_value_map(value) if errors: for e in errors: log.error(e) @@ -334,7 +334,7 @@ def matrix( └── Containerfile*.jinja2 ``` """ - value_map, value_errors = __make_value_map(value) + value_map, value_errors = _make_value_map(value) parsed_dependency_constraints = [] dependency_constraint_errors = [] diff --git a/posit-bakery/posit_bakery/cli/run.py b/posit-bakery/posit_bakery/cli/run.py index 67ccbf65..43d3a29c 100644 --- a/posit-bakery/posit_bakery/cli/run.py +++ b/posit-bakery/posit_bakery/cli/run.py @@ -7,7 +7,7 @@ import typer -from posit_bakery.cli.common import with_verbosity_flags +from posit_bakery.cli.common import _make_value_map, with_verbosity_flags from posit_bakery.config import BakeryConfig from posit_bakery.config.config import BakeryConfigFilter, BakerySettings from posit_bakery.const import DevVersionInclusionEnum, MatrixVersionInclusionEnum @@ -97,6 +97,20 @@ def dgoss( rich_help_panel=RichHelpPanelEnum.FILTERS, ), ] = MatrixVersionInclusionEnum.EXCLUDE, + dev_stream: Annotated[ + Optional[str], + typer.Option( + help="Filter development versions to a specific release stream (e.g. 'daily', 'preview').", + rich_help_panel=RichHelpPanelEnum.FILTERS, + ), + ] = None, + value: Annotated[ + Optional[list[str]], + typer.Option( + help="Override a devVersion value (key=value). Can be specified multiple times.", + rich_help_panel=RichHelpPanelEnum.FILTERS, + ), + ] = None, metadata_file: Annotated[ Optional[Path], typer.Option( @@ -130,9 +144,13 @@ def dgoss( DeprecationWarning, stacklevel=2, ) - stderr_console.print( - "[yellow]Warning: 'bakery run dgoss' is deprecated. Use 'bakery dgoss run' instead.[/yellow]" - ) + stderr_console.print("[yellow]Warning: 'bakery run dgoss' is deprecated. Use 'bakery dgoss run' instead.[/yellow]") + + value_map, errors = _make_value_map(value) + if errors: + for e in errors: + log.error(e) + raise typer.Exit(code=1) # Autoselect host architecture platform if not specified. image_platform = image_platform or SETTINGS.architecture @@ -145,6 +163,8 @@ def dgoss( image_variant=image_variant, image_os=image_os, image_platform=[image_platform], + dev_stream=dev_stream, + values=value_map, ), dev_versions=dev_versions, matrix_versions=matrix_versions, diff --git a/posit-bakery/posit_bakery/cli/update.py b/posit-bakery/posit_bakery/cli/update.py index ed487f11..786e74e6 100644 --- a/posit-bakery/posit_bakery/cli/update.py +++ b/posit-bakery/posit_bakery/cli/update.py @@ -5,7 +5,7 @@ import typer -from posit_bakery.cli.common import __make_value_map, with_verbosity_flags +from posit_bakery.cli.common import _make_value_map, with_verbosity_flags from posit_bakery.config import BakeryConfig from posit_bakery.config.config import BakeryConfigFilter from posit_bakery.log import stderr_console @@ -141,7 +141,7 @@ def version( bakery update version connect 2026.03.1 --target-version 2026.03.0 # Explicitly patches '2026.03.0' to '2026.03.1' """ - value_map, errors = __make_value_map(value) + value_map, errors = _make_value_map(value) if errors: for e in errors: log.error(e) diff --git a/posit-bakery/posit_bakery/config/config.py b/posit-bakery/posit_bakery/config/config.py index 16847925..b69bb54a 100644 --- a/posit-bakery/posit_bakery/config/config.py +++ b/posit-bakery/posit_bakery/config/config.py @@ -264,6 +264,20 @@ class BakeryConfigFilter(BaseModel): image_platform: Annotated[ list[str], Field(description="Name or regex pattern of the image platform to filter by.", default_factory=list) ] + dev_stream: Annotated[ + str | None, + Field( + description="Development stream to filter by (e.g. 'daily', 'preview').", + default=None, + ), + ] + values: Annotated[ + dict[str, str], + Field( + description="Key-value pairs to override in devVersion values (e.g. channel=apple-blossom).", + default_factory=dict, + ), + ] class BakerySettings(BaseModel): @@ -331,7 +345,10 @@ def __init__(self, config_file: str | Path | os.PathLike, settings: BakerySettin if self.settings.dev_versions in [DevVersionInclusionEnum.ONLY, DevVersionInclusionEnum.INCLUDE]: for image in self.model.images: - image.load_dev_versions() + image.load_dev_versions( + dev_stream=self.settings.filter.dev_stream, + values=self.settings.filter.values, + ) image.render_ephemeral_version_files() if self.settings.clean_temporary: atexit.register(image.remove_ephemeral_version_files) diff --git a/posit-bakery/posit_bakery/config/image/dev_version/base.py b/posit-bakery/posit_bakery/config/image/dev_version/base.py index 4162991f..5f8eaf86 100644 --- a/posit-bakery/posit_bakery/config/image/dev_version/base.py +++ b/posit-bakery/posit_bakery/config/image/dev_version/base.py @@ -194,9 +194,10 @@ def all_registries(self) -> list[Registry | BaseRegistry]: return all_registries @abc.abstractmethod - def get_version(self) -> str: + def get_version(self, values: dict[str, str] | None = None) -> str: """Retrieve the version string for this image development version. + :param values: Optional merged values dict (self.values + overrides). If None, uses self.values. :return: The version string. """ raise NotImplementedError("Subclasses must implement get_version method.") @@ -221,16 +222,21 @@ def add_os_url(self) -> "BaseImageDevelopmentVersion": return self - def as_image_version(self): - """Convert this development version to a standard image version.""" + def as_image_version(self, value_overrides: dict[str, str] | None = None): + """Convert this development version to a standard image version. + + :param value_overrides: Optional key-value pairs to merge on top of self.values. + Does not mutate the original values dict. + """ + merged_values = {**self.values, **value_overrides} if value_overrides else self.values return ImageVersion( - name=self.get_version(), - subpath=f".dev-{self.get_version()}".replace(" ", "-").lower(), + name=self.get_version(merged_values), + subpath=f".dev-{self.get_version(merged_values)}".replace(" ", "-").lower(), parent=self.parent, extraRegistries=self.extraRegistries, overrideRegistries=self.overrideRegistries, os=self.os, - values=self.values, + values=merged_values, latest=False, dependencies=self.parent.resolve_dependency_versions(), ephemeral=True, diff --git a/posit-bakery/posit_bakery/config/image/dev_version/env.py b/posit-bakery/posit_bakery/config/image/dev_version/env.py index f0b00a89..4844cf3f 100644 --- a/posit-bakery/posit_bakery/config/image/dev_version/env.py +++ b/posit-bakery/posit_bakery/config/image/dev_version/env.py @@ -51,9 +51,10 @@ def validate_env_vars(cls, v: str, info: ValidationInfo): return v - def get_version(self) -> str: + def get_version(self, values: dict[str, str] | None = None) -> str: """Retrieve the version from the specified environment variable. + :param values: Unused. Accepted for ABC compatibility. :return: The version string from the environment variable. """ return _get_value_from_env("versionEnvVar", self.versionEnvVar) diff --git a/posit-bakery/posit_bakery/config/image/dev_version/stream.py b/posit-bakery/posit_bakery/config/image/dev_version/stream.py index 0aa0f7ad..169d6eca 100644 --- a/posit-bakery/posit_bakery/config/image/dev_version/stream.py +++ b/posit-bakery/posit_bakery/config/image/dev_version/stream.py @@ -27,13 +27,16 @@ def get_primary_os(self) -> ImageVersionOS: return DEFAULT_OS - def get_version(self) -> str: + def get_version(self, values: dict[str, str] | None = None) -> str: """Retrieve the version from the specified product stream. + :param values: Optional merged values dict. If None, uses self.values. :return: The version string from the product stream. """ _os = self.get_primary_os() - result = get_product_artifact_by_stream(self.product, self.stream, _os.buildOS) + result = get_product_artifact_by_stream( + self.product, self.stream, _os.buildOS, values=values if values is not None else self.values + ) return result.version @@ -44,7 +47,7 @@ def get_url_by_os(self, generalize_architecture: bool = False) -> dict[str, str] """ url_by_os = {} for _os in self.os: - result = get_product_artifact_by_stream(self.product, self.stream, _os.buildOS) + result = get_product_artifact_by_stream(self.product, self.stream, _os.buildOS, values=self.values) if generalize_architecture: url_by_os[_os.name] = str(result.architecture_generalized_download_url) else: diff --git a/posit-bakery/posit_bakery/config/image/image.py b/posit-bakery/posit_bakery/config/image/image.py index 744b0660..97fb6b8b 100644 --- a/posit-bakery/posit_bakery/config/image/image.py +++ b/posit-bakery/posit_bakery/config/image/image.py @@ -573,10 +573,22 @@ def patch_version( return patched_image_version - def load_dev_versions(self): - """Load the development versions for this image.""" + def load_dev_versions(self, dev_stream: str | None = None, values: dict[str, str] | None = None): + """Load the development versions for this image. + + :param dev_stream: If provided, only load dev versions from this stream. + :param values: Key-value pairs to override in each devVersion's values dict. + """ for dev_version in self.devVersions: - image_version = dev_version.as_image_version() + if dev_stream is not None and hasattr(dev_version, "stream"): + if dev_version.stream.value != dev_stream: + log.info( + f"Skipping {self.name} dev version {repr(dev_version)}: " + f"stream '{dev_version.stream.value}' does not match filter '{dev_stream}'" + ) + continue + + image_version = dev_version.as_image_version(value_overrides=values) log_message = f"Loaded {self.name} development version from {repr(dev_version)}:\n" log_message += f" - Version: {image_version.name}\n" for dep in image_version.dependencies: diff --git a/posit-bakery/posit_bakery/config/image/posit_product/main.py b/posit-bakery/posit_bakery/config/image/posit_product/main.py index dab7d276..1412378b 100644 --- a/posit-bakery/posit_bakery/config/image/posit_product/main.py +++ b/posit-bakery/posit_bakery/config/image/posit_product/main.py @@ -43,8 +43,15 @@ def __init__(self, stream_url: str, resolver_map: dict[str, resolvers.AbstractRe def get(self, metadata: dict) -> ReleaseStreamResult: """Fetches data from the stream URL and resolves the data using the given resolvers.""" + try: + stream_url = self.stream_url.format_map(metadata) + except KeyError as e: + raise ValueError( + f"Stream URL {self.stream_url!r} contains placeholder {e} " + f"not found in metadata. Pass --value {e.args[0]}= to set it." + ) from e session = cached_session() - response = session.get(self.stream_url) + response = session.get(stream_url) response.raise_for_status() try: data = response.json() @@ -281,7 +288,11 @@ def _make_resolver_metadata(_os: BuildOS, product: ProductEnum): def get_product_artifact_by_stream( - product: ProductEnum, stream: ReleaseStreamEnum, os: BuildOS, generalize_arch: bool = True + product: ProductEnum, + stream: ReleaseStreamEnum, + os: BuildOS, + generalize_arch: bool = True, + values: dict[str, str] | None = None, ) -> ReleaseStreamResult: """Fetches the version and download URL for a given product, release stream, and OS.""" if product not in product_release_stream_url_map: @@ -290,5 +301,7 @@ def get_product_artifact_by_stream( raise ValueError(f"Stream {stream} is not supported for product {product}.") metadata = _make_resolver_metadata(os, product) + if values: + metadata.update(values) return product_release_stream_url_map[product][stream].get(metadata) diff --git a/posit-bakery/test/cli/test_common.py b/posit-bakery/test/cli/test_common.py index 8ffb155b..dcfc2def 100644 --- a/posit-bakery/test/cli/test_common.py +++ b/posit-bakery/test/cli/test_common.py @@ -3,7 +3,7 @@ import pytest from posit_bakery.cli.common import ( - __make_value_map as make_value_map, + _make_value_map as make_value_map, __parse_dependency_constraint as parse_dependency_constraint, __parse_dependency_versions as parse_dependency_versions, ) @@ -22,7 +22,7 @@ class TestMakeValueMap: - """Tests for the __make_value_map function""" + """Tests for the _make_value_map function""" def test_none_input(self): """Test that None input returns empty dict with no errors""" diff --git a/posit-bakery/test/config/image/posit_products/test_main.py b/posit-bakery/test/config/image/posit_products/test_main.py index 4d3bc283..fdcf407f 100644 --- a/posit-bakery/test/config/image/posit_products/test_main.py +++ b/posit-bakery/test/config/image/posit_products/test_main.py @@ -1,3 +1,5 @@ +from unittest.mock import MagicMock, patch + import pytest from posit_bakery.config.image.build_os import SUPPORTED_OS, BuildOS @@ -6,6 +8,7 @@ _parse_download_json_os_identifier, _make_resolver_metadata, get_product_artifact_by_stream, + ReleaseStreamPath, ReleaseStreamResult, ) @@ -163,6 +166,48 @@ ] +class TestReleaseStreamPath: + @pytest.fixture + def _mock_session(self): + """Patch cached_session to return a mock that accepts any URL.""" + mock_resp = MagicMock() + mock_resp.json.return_value = {} + mock_resp.raise_for_status.return_value = None + mock_session = MagicMock() + mock_session.get.return_value = mock_resp + with patch("posit_bakery.config.image.posit_product.main.cached_session", return_value=mock_session): + yield mock_session + + def test_static_url(self, _mock_session): + """A URL with no placeholders passes through unchanged.""" + path = ReleaseStreamPath( + "https://example.com/daily.json", + {"version": "1.0.0", "download_url": "https://example.com/pkg.deb"}, + ) + result = path.get({"os": "ubuntu"}) + assert result.version == "1.0.0" + _mock_session.get.assert_called_once_with("https://example.com/daily.json") + + def test_url_with_placeholder(self, _mock_session): + """A URL with a {channel} placeholder resolves from metadata.""" + path = ReleaseStreamPath( + "https://dailies.example.com/{channel}/index.json", + {"version": "2.0.0", "download_url": "https://example.com/pkg.deb"}, + ) + result = path.get({"channel": "apple-blossom"}) + assert result.version == "2.0.0" + _mock_session.get.assert_called_once_with("https://dailies.example.com/apple-blossom/index.json") + + def test_url_with_missing_placeholder_raises(self): + """A URL with a placeholder not in metadata raises ValueError.""" + path = ReleaseStreamPath( + "https://dailies.example.com/{channel}/index.json", + {"version": "2.0.0", "download_url": "https://example.com/pkg.deb"}, + ) + with pytest.raises(ValueError, match="channel"): + path.get({}) + + class TestReleaseStreamResult: @pytest.mark.parametrize( "download_url,expected_url", diff --git a/posit-bakery/test/config/image/test_image.py b/posit-bakery/test/config/image/test_image.py index b0812882..8d6ad707 100644 --- a/posit-bakery/test/config/image/test_image.py +++ b/posit-bakery/test/config/image/test_image.py @@ -704,6 +704,111 @@ def test_load_dev_versions(self): assert i.get_version(stream_version).os[0].name == "Ubuntu 22.04" assert str(i.get_version(stream_version).os[0].artifactDownloadURL) == stream_url + def test_load_dev_versions_dev_stream_filter(self): + """Test that load_dev_versions filters by dev_stream when provided.""" + context = Path(__file__).parent.parent.parent / "contexts" / "with-dev-versions" + mock_parent = MagicMock(spec=BakeryConfigDocument) + mock_parent.path = context + + stream_version = "1.1.0" + stream_url = "https://example.com/image-daily.tar.gz" + with patch("posit_bakery.config.image.dev_version.stream.get_product_artifact_by_stream") as mock_get: + mock_get.return_value = ReleaseStreamResult(version=stream_version, download_url=stream_url) + i = Image( + name="my-image", + parent=mock_parent, + devVersions=[ + { + "sourceType": "stream", + "product": "package-manager", + "stream": "daily", + "os": [{"name": "Ubuntu 22.04", "primary": True}], + }, + { + "sourceType": "stream", + "product": "package-manager", + "stream": "preview", + "os": [{"name": "Ubuntu 22.04", "primary": True}], + }, + ], + versions=[{"name": "1.0.0"}], + ) + i.load_dev_versions(dev_stream="daily") + + # Only the daily stream should be loaded; preview should be skipped. + # 1.0.0 (release) + daily dev version = 2 total. Preview is filtered out. + assert len(i.versions) == 2 + assert i.get_version("1.0.0") is not None + assert i.get_version(stream_version) is not None + assert i.get_version(stream_version).isDevelopmentVersion + + def test_load_dev_versions_values_override(self): + """Test that load_dev_versions applies value overrides to dev versions.""" + context = Path(__file__).parent.parent.parent / "contexts" / "with-dev-versions" + mock_parent = MagicMock(spec=BakeryConfigDocument) + mock_parent.path = context + + stream_version = "1.1.0" + stream_url = "https://example.com/image-daily.tar.gz" + with patch("posit_bakery.config.image.dev_version.stream.get_product_artifact_by_stream") as mock_get: + mock_get.return_value = ReleaseStreamResult(version=stream_version, download_url=stream_url) + i = Image( + name="my-image", + parent=mock_parent, + devVersions=[ + { + "sourceType": "stream", + "product": "workbench", + "stream": "daily", + "os": [{"name": "Ubuntu 22.04", "primary": True}], + "values": {"channel": "latest"}, + }, + ], + versions=[{"name": "1.0.0"}], + ) + i.load_dev_versions(values={"channel": "apple-blossom"}) + + # The original devVersion model must not be mutated. + assert i.devVersions[0].values["channel"] == "latest" + # The override should flow through to the resulting image version. + dev_ver = i.get_version(stream_version) + assert dev_ver is not None + assert dev_ver.values["channel"] == "apple-blossom" + + def test_load_dev_versions_values_override_channel_url(self): + """Test that a channel override propagates to get_product_artifact_by_stream as metadata.""" + context = Path(__file__).parent.parent.parent / "contexts" / "with-dev-versions" + mock_parent = MagicMock(spec=BakeryConfigDocument) + mock_parent.path = context + + stream_version = "2026.04.0-daily+313.pro27" + stream_url = "https://dailies.rstudio.com/rstudio/globemaster-allium/workbench.deb" + with patch("posit_bakery.config.image.dev_version.stream.get_product_artifact_by_stream") as mock_get: + mock_get.return_value = ReleaseStreamResult(version=stream_version, download_url=stream_url) + i = Image( + name="workbench", + parent=mock_parent, + devVersions=[ + { + "sourceType": "stream", + "product": "workbench", + "stream": "daily", + "os": [{"name": "Ubuntu 24.04", "primary": True}], + "values": {"channel": "latest"}, + }, + ], + versions=[{"name": "2026.03.0"}], + ) + i.load_dev_versions(values={"channel": "globemaster-allium"}) + + # The original devVersion model must not be mutated. + assert i.devVersions[0].values["channel"] == "latest" + # The overridden value should be passed to get_product_artifact_by_stream. + assert mock_get.called + assert any( + call.kwargs.get("values", {}).get("channel") == "globemaster-allium" for call in mock_get.call_args_list + ) + def test_render_ephemeral_version_files(self, get_tmpcontext, common_image_variants_objects): """Test that render_ephemeral_version_files creates the correct directory structure for an ephemeral version.""" context = get_tmpcontext("basic")