From a8f9fbb77d780705cb292e2a6b7ba109b3038f50 Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Fri, 10 Apr 2026 11:32:07 -0500 Subject: [PATCH 01/15] Fix script injection in build workflows Move all ${{ }} expressions out of run: blocks into env: blocks to prevent script injection from runtime values (matrix outputs, secrets). This makes the "no expressions in run blocks" rule enforceable by zizmor without per-expression exceptions. - bakery-build-native.yml: matrix, build-test (filter, build, test), merge (filter, merge/push), readme steps - bakery-build.yml: matrix, build (filter, build, test, push), readme steps - Convert conditional push flag to shell logic - Quote all shell variable references --- .github/workflows/bakery-build-native.yml | 122 +++++++++++++--------- .github/workflows/bakery-build.yml | 92 ++++++++++------ 2 files changed, 132 insertions(+), 82 deletions(-) diff --git a/.github/workflows/bakery-build-native.yml b/.github/workflows/bakery-build-native.yml index 3c1942aa1..74575c028 100644 --- a/.github/workflows/bakery-build-native.yml +++ b/.github/workflows/bakery-build-native.yml @@ -75,6 +75,12 @@ defaults: run: shell: bash +# Security policy: No ${{ }} expressions in `run:` blocks. +# All expression values are assigned to `env:` and referenced as +# shell variables. This prevents script injection from runtime values +# (matrix outputs, secrets) and keeps the rule enforceable by zizmor +# without per-expression exceptions. + jobs: matrix: name: Image Matrix @@ -94,12 +100,20 @@ jobs: - name: Images by Version/Platform id: images-by-platform + env: + DEV_VERSIONS: ${{ inputs.dev-versions }} + MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + CONTEXT: ${{ inputs.context }} run: | - echo "platform_matrix=$(bakery ci matrix --quiet --dev-versions ${{ inputs.dev-versions }} --matrix-versions ${{ inputs.matrix-versions }} --context ${{ inputs.context }} | jq --compact-output .)" >> $GITHUB_OUTPUT + echo "platform_matrix=$(bakery ci matrix --quiet --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS" --context "$CONTEXT" | jq --compact-output .)" >> $GITHUB_OUTPUT - name: Images by Version id: images-by-version + env: + DEV_VERSIONS: ${{ inputs.dev-versions }} + MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + CONTEXT: ${{ inputs.context }} run: | - echo "versions_matrix=$(bakery ci matrix --quiet --dev-versions ${{ inputs.dev-versions }} --matrix-versions ${{ inputs.matrix-versions }} --exclude platform --context ${{ inputs.context }} | jq --compact-output .)" >> $GITHUB_OUTPUT + echo "versions_matrix=$(bakery ci matrix --quiet --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS" --exclude platform --context "$CONTEXT" | jq --compact-output .)" >> $GITHUB_OUTPUT build-test: name: "Build/Test ${{ matrix.img.image }}:${{ matrix.img.version }} (${{ matrix.img.platform }})" @@ -141,18 +155,12 @@ jobs: # this step sets an output that we can reference later. - name: Filter Steps id: filter-steps + env: + HAS_DOCKER_HUB: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN != '' }} + HAS_AWS_ROLE: ${{ secrets.AWS_ROLE != '' }} run: | - if [ -n "${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}" ] ; then - echo "docker-hub=true" >> $GITHUB_OUTPUT - else - echo "docker-hub=false" >> $GITHUB_OUTPUT - fi - - if [ -n "${{ secrets.AWS_ROLE }}" ] ; then - echo "ecr=true" >> $GITHUB_OUTPUT - else - echo "ecr=false" >> $GITHUB_OUTPUT - fi + echo "docker-hub=$HAS_DOCKER_HUB" >> $GITHUB_OUTPUT + echo "ecr=$HAS_AWS_ROLE" >> $GITHUB_OUTPUT - name: Login to GitHub Container Registry uses: docker/login-action@v4 @@ -190,35 +198,50 @@ jobs: - name: Build env: GIT_SHA: ${{ github.sha }} + RETRY: ${{ inputs.retry }} + IMAGE_NAME: ${{ matrix.img.image }} + IMAGE_VERSION: ${{ matrix.img.version }} + IMAGE_PLATFORM: ${{ matrix.img.platform }} + DEV_VERSIONS: ${{ inputs.dev-versions }} + MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + 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: | - PLATFORM=${BUILD_PLATFORM#linux/} \ bakery build \ --strategy build --pull \ - --retry ${{ inputs.retry }} \ - --image-name '^${{ matrix.img.image }}$' \ - --image-version ${{ matrix.img.version }} \ - --image-platform ${{ matrix.img.platform }} \ - --dev-versions ${{ inputs.dev-versions }} \ - --matrix-versions ${{ inputs.matrix-versions }} \ - --cache-registry "ghcr.io/${{ github.repository_owner }}" \ - --temp-registry "ghcr.io/${{ github.repository_owner }}" \ - --metadata-file "./${{ matrix.img.image }}-${{ matrix.img.version }}-${{ steps.normalize-platform.outputs.platform }}-metadata.json" \ - --context ${{ inputs.context }} \ + --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 - name: Test + env: + IMAGE_NAME: ${{ matrix.img.image }} + IMAGE_VERSION: ${{ matrix.img.version }} + IMAGE_PLATFORM: ${{ matrix.img.platform }} + DEV_VERSIONS: ${{ inputs.dev-versions }} + MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + NORMALIZED_PLATFORM: ${{ steps.normalize-platform.outputs.platform }} + CONTEXT: ${{ inputs.context }} run: | - PLATFORM=${BUILD_PLATFORM#linux/} \ GOSS_PATH=${GITHUB_WORKSPACE}/tools/goss \ DGOSS_PATH=${GITHUB_WORKSPACE}/tools/dgoss \ bakery run dgoss \ - --image-name '^${{ matrix.img.image }}$' \ - --image-version ${{ matrix.img.version }} \ - --image-platform ${{ matrix.img.platform }} \ - --dev-versions ${{ inputs.dev-versions }} \ - --matrix-versions ${{ inputs.matrix-versions }} \ - --metadata-file "./${{ matrix.img.image }}-${{ matrix.img.version }}-${{ steps.normalize-platform.outputs.platform }}-metadata.json" \ - --context ${{ inputs.context }} + --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" - name: Upload Metadata uses: actions/upload-artifact@v7 with: @@ -260,18 +283,12 @@ jobs: # this step sets an output that we can reference later. - name: Filter Steps id: filter-steps + env: + HAS_DOCKER_HUB: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN != '' }} + HAS_AWS_ROLE: ${{ secrets.AWS_ROLE != '' }} run: | - if [ -n "${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}" ] ; then - echo "docker-hub=true" >> $GITHUB_OUTPUT - else - echo "docker-hub=false" >> $GITHUB_OUTPUT - fi - - if [ -n "${{ secrets.AWS_ROLE }}" ] ; then - echo "ecr=true" >> $GITHUB_OUTPUT - else - echo "ecr=false" >> $GITHUB_OUTPUT - fi + echo "docker-hub=$HAS_DOCKER_HUB" >> $GITHUB_OUTPUT + echo "ecr=$HAS_AWS_ROLE" >> $GITHUB_OUTPUT - name: Login to GitHub Container Registry uses: docker/login-action@v4 @@ -318,11 +335,15 @@ jobs: - name: Merge/Push env: GIT_SHA: ${{ github.sha }} + CONTEXT: ${{ inputs.context }} + REGISTRY: ghcr.io/${{ github.repository_owner }} + PUSH: ${{ inputs.push }} run: | + if [ "$PUSH" = "true" ]; then PUSH_FLAG=""; else PUSH_FLAG="--dry-run"; fi bakery ci merge \ - --context ${{ inputs.context }} \ - --temp-registry "ghcr.io/${{ github.repository_owner }}" \ - ${{ inputs.push && ' \' || '--dry-run \' }} + --context "$CONTEXT" \ + --temp-registry "$REGISTRY" \ + $PUSH_FLAG \ *-metadata.json readme: @@ -345,8 +366,11 @@ jobs: env: DOCKER_HUB_README_USERNAME: ${{ secrets.DOCKER_HUB_README_USERNAME }} DOCKER_HUB_README_PASSWORD: ${{ secrets.DOCKER_HUB_README_PASSWORD }} + CONTEXT: ${{ inputs.context }} + DEV_VERSIONS: ${{ inputs.dev-versions }} + MATRIX_VERSIONS: ${{ inputs.matrix-versions }} run: | bakery ci readme \ - --context ${{ inputs.context }} \ - --dev-versions ${{ inputs.dev-versions }} \ - --matrix-versions ${{ inputs.matrix-versions }} + --context "$CONTEXT" \ + --dev-versions "$DEV_VERSIONS" \ + --matrix-versions "$MATRIX_VERSIONS" diff --git a/.github/workflows/bakery-build.yml b/.github/workflows/bakery-build.yml index c040be02a..76f5043da 100644 --- a/.github/workflows/bakery-build.yml +++ b/.github/workflows/bakery-build.yml @@ -66,6 +66,12 @@ defaults: run: shell: bash +# Security policy: No ${{ }} expressions in `run:` blocks. +# All expression values are assigned to `env:` and referenced as +# shell variables. This prevents script injection from runtime values +# (matrix outputs, secrets) and keeps the rule enforceable by zizmor +# without per-expression exceptions. + jobs: matrix: name: Image Matrix @@ -84,8 +90,12 @@ jobs: - name: Images id: images + env: + DEV_VERSIONS: ${{ inputs.dev-versions }} + MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + CONTEXT: ${{ inputs.context }} run: | - echo "matrix=$(bakery ci matrix --quiet --dev-versions ${{ inputs.dev-versions }} --matrix-versions ${{ inputs.matrix-versions }} --context ${{ inputs.context }} | jq --compact-output .)" >> $GITHUB_OUTPUT + echo "matrix=$(bakery ci matrix --quiet --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS" --context "$CONTEXT" | jq --compact-output .)" >> $GITHUB_OUTPUT build: name: "${{ matrix.img.image }}:${{ matrix.img.version }}" @@ -115,18 +125,12 @@ jobs: # this step sets an output that we can reference later. - name: Filter Steps id: filter-steps + env: + HAS_DOCKER_HUB: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN != '' }} + HAS_AWS_ROLE: ${{ secrets.AWS_ROLE != '' }} run: | - if [ -n "${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}" ] ; then - echo "docker-hub=true" >> $GITHUB_OUTPUT - else - echo "docker-hub=false" >> $GITHUB_OUTPUT - fi - - if [ -n "${{ secrets.AWS_ROLE }}" ] ; then - echo "ecr=true" >> $GITHUB_OUTPUT - else - echo "ecr=false" >> $GITHUB_OUTPUT - fi + echo "docker-hub=$HAS_DOCKER_HUB" >> $GITHUB_OUTPUT + echo "ecr=$HAS_AWS_ROLE" >> $GITHUB_OUTPUT - name: Login to GitHub Container Registry uses: docker/login-action@v4 @@ -160,26 +164,39 @@ jobs: - name: Build env: GIT_SHA: ${{ github.sha }} + RETRY: ${{ inputs.retry }} + IMAGE_NAME: ${{ matrix.img.image }} + IMAGE_VERSION: ${{ matrix.img.version }} + DEV_VERSIONS: ${{ inputs.dev-versions }} + MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + REGISTRY: ghcr.io/${{ github.repository_owner }} + CONTEXT: ${{ inputs.context }} run: | bakery build --load --pull \ - --retry ${{ inputs.retry }} \ - --image-name '^${{ matrix.img.image }}$' \ - --image-version ${{ matrix.img.version }} \ - --dev-versions ${{ inputs.dev-versions }} \ - --matrix-versions ${{ inputs.matrix-versions }} \ - --cache-registry "ghcr.io/${{ github.repository_owner }}" \ - --context ${{ inputs.context }} + --retry "$RETRY" \ + --image-name "^${IMAGE_NAME}$" \ + --image-version "$IMAGE_VERSION" \ + --dev-versions "$DEV_VERSIONS" \ + --matrix-versions "$MATRIX_VERSIONS" \ + --cache-registry "$REGISTRY" \ + --context "$CONTEXT" - name: Test + env: + IMAGE_NAME: ${{ matrix.img.image }} + IMAGE_VERSION: ${{ matrix.img.version }} + DEV_VERSIONS: ${{ inputs.dev-versions }} + MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + CONTEXT: ${{ inputs.context }} run: | GOSS_PATH=${GITHUB_WORKSPACE}/tools/goss \ DGOSS_PATH=${GITHUB_WORKSPACE}/tools/dgoss \ bakery run dgoss \ - --image-name '^${{ matrix.img.image }}$' \ - --image-version ${{ matrix.img.version }} \ - --dev-versions ${{ inputs.dev-versions }} \ - --matrix-versions ${{ inputs.matrix-versions }} \ - --context ${{ inputs.context }} + --image-name "^${IMAGE_NAME}$" \ + --image-version "$IMAGE_VERSION" \ + --dev-versions "$DEV_VERSIONS" \ + --matrix-versions "$MATRIX_VERSIONS" \ + --context "$CONTEXT" - name: Push # Since this is a reusable workflow, we need to be very explicit about @@ -188,14 +205,20 @@ jobs: if: ${{ inputs.push }} env: GIT_SHA: ${{ github.sha }} + RETRY: ${{ inputs.retry }} + IMAGE_NAME: ${{ matrix.img.image }} + IMAGE_VERSION: ${{ matrix.img.version }} + DEV_VERSIONS: ${{ inputs.dev-versions }} + MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + CONTEXT: ${{ inputs.context }} run: | bakery build --push --pull \ - --retry ${{ inputs.retry }} \ - --image-name '^${{ matrix.img.image }}$' \ - --image-version ${{ matrix.img.version }} \ - --dev-versions ${{ inputs.dev-versions }} \ - --matrix-versions ${{ inputs.matrix-versions }} \ - --context ${{ inputs.context }} + --retry "$RETRY" \ + --image-name "^${IMAGE_NAME}$" \ + --image-version "$IMAGE_VERSION" \ + --dev-versions "$DEV_VERSIONS" \ + --matrix-versions "$MATRIX_VERSIONS" \ + --context "$CONTEXT" readme: name: Push READMEs @@ -217,8 +240,11 @@ jobs: env: DOCKER_HUB_README_USERNAME: ${{ secrets.DOCKER_HUB_README_USERNAME }} DOCKER_HUB_README_PASSWORD: ${{ secrets.DOCKER_HUB_README_PASSWORD }} + CONTEXT: ${{ inputs.context }} + DEV_VERSIONS: ${{ inputs.dev-versions }} + MATRIX_VERSIONS: ${{ inputs.matrix-versions }} run: | bakery ci readme \ - --context ${{ inputs.context }} \ - --dev-versions ${{ inputs.dev-versions }} \ - --matrix-versions ${{ inputs.matrix-versions }} + --context "$CONTEXT" \ + --dev-versions "$DEV_VERSIONS" \ + --matrix-versions "$MATRIX_VERSIONS" From fdf1dd90940900ff6490cd196581c3f8e503b32d Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Fri, 10 Apr 2026 11:33:25 -0500 Subject: [PATCH 02/15] Fix script injection in auxiliary workflows Move all ${{ }} expressions out of run: blocks into env: blocks in clean.yml, product-release.yml, hadolint.yml, and ci.yml (release job). - clean.yml: Convert conditional flags (dry-run, untagged, older-than) from expression ternaries to shell logic - product-release.yml: Move inputs.version and step outputs to env vars - hadolint.yml: Move inputs.context to env var - ci.yml: Move github.ref_name to env var in release step --- .github/workflows/ci.yml | 5 ++- .github/workflows/clean.yml | 54 +++++++++++++++++++------- .github/workflows/hadolint.yml | 56 +++++++++++++++++++++++++++ .github/workflows/product-release.yml | 16 ++++++-- 4 files changed, 112 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/hadolint.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5abc5bb20..140b80f8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -205,10 +205,11 @@ jobs: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REF_NAME: ${{ github.ref_name }} run: | - gh release create ${{ github.ref_name }} \ + gh release create "$REF_NAME" \ --draft \ --generate-notes \ --latest - gh release upload ${{ github.ref_name }} \ + gh release upload "$REF_NAME" \ ./posit-bakery/dist/* diff --git a/.github/workflows/clean.yml b/.github/workflows/clean.yml index b38a75761..7ed47ca3a 100644 --- a/.github/workflows/clean.yml +++ b/.github/workflows/clean.yml @@ -66,6 +66,12 @@ defaults: run: shell: bash +# Security policy: No ${{ }} expressions in `run:` blocks. +# All expression values are assigned to `env:` and referenced as +# shell variables. This prevents script injection from runtime values +# (matrix outputs, secrets) and keeps the rule enforceable by zizmor +# without per-expression exceptions. + jobs: clean-caches: name: Clean Caches @@ -91,14 +97,24 @@ jobs: - name: Clean caches env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REGISTRY: ghcr.io/${{ github.repository_owner }} + CONTEXT: ${{ inputs.context }} + DEV_VERSIONS: ${{ inputs.dev-versions }} + MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + DRY_RUN: ${{ inputs.dry-run }} + REMOVE_DANGLING: ${{ inputs.remove-dangling-caches }} + OLDER_THAN: ${{ inputs.remove-caches-older-than }} run: | - bakery clean cache-registry "ghcr.io/${{ github.repository_owner }}" \ - --context "${{ inputs.context }}" \ - --dev-versions "${{ inputs['dev-versions'] }}" \ - --matrix-versions "${{ inputs['matrix-versions'] }}" \ - ${{ inputs.dry-run && '--dry-run' || '' }} \ - ${{ !inputs.remove-dangling-caches && '--no-untagged' || '--untagged' }} \ - ${{ inputs.remove-caches-older-than > 0 && format('--older-than {0}', inputs.remove-caches-older-than) || '' }} + DRY_RUN_FLAG=""; if [ "$DRY_RUN" = "true" ]; then DRY_RUN_FLAG="--dry-run"; fi + UNTAGGED_FLAG="--no-untagged"; if [ "$REMOVE_DANGLING" = "true" ]; then UNTAGGED_FLAG="--untagged"; fi + OLDER_FLAG=""; if [ "$OLDER_THAN" -gt 0 ] 2>/dev/null; then OLDER_FLAG="--older-than $OLDER_THAN"; fi + bakery clean cache-registry "$REGISTRY" \ + --context "$CONTEXT" \ + --dev-versions "$DEV_VERSIONS" \ + --matrix-versions "$MATRIX_VERSIONS" \ + $DRY_RUN_FLAG \ + $UNTAGGED_FLAG \ + $OLDER_FLAG clean-temp: name: Clean Temporary Images @@ -124,11 +140,21 @@ jobs: - name: Clean temporary images env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REGISTRY: ghcr.io/${{ github.repository_owner }} + CONTEXT: ${{ inputs.context }} + DEV_VERSIONS: ${{ inputs.dev-versions }} + MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + DRY_RUN: ${{ inputs.dry-run }} + REMOVE_DANGLING: ${{ inputs.remove-dangling-temporary-images }} + OLDER_THAN: ${{ inputs.remove-temporary-images-older-than }} run: | - bakery clean temp-registry "ghcr.io/${{ github.repository_owner }}" \ - --context "${{ inputs.context }}" \ - --dev-versions "${{ inputs['dev-versions'] }}" \ - --matrix-versions "${{ inputs['matrix-versions'] }}" \ - ${{ inputs.dry-run && '--dry-run' || '' }} \ - ${{ !inputs.remove-dangling-temporary-images && '--no-untagged' || '--untagged' }} \ - ${{ inputs.remove-temporary-images-older-than > 0 && format('--older-than {0}', inputs.remove-temporary-images-older-than) || '' }} + DRY_RUN_FLAG=""; if [ "$DRY_RUN" = "true" ]; then DRY_RUN_FLAG="--dry-run"; fi + UNTAGGED_FLAG="--no-untagged"; if [ "$REMOVE_DANGLING" = "true" ]; then UNTAGGED_FLAG="--untagged"; fi + OLDER_FLAG=""; if [ "$OLDER_THAN" -gt 0 ] 2>/dev/null; then OLDER_FLAG="--older-than $OLDER_THAN"; fi + bakery clean temp-registry "$REGISTRY" \ + --context "$CONTEXT" \ + --dev-versions "$DEV_VERSIONS" \ + --matrix-versions "$MATRIX_VERSIONS" \ + $DRY_RUN_FLAG \ + $UNTAGGED_FLAG \ + $OLDER_FLAG diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml new file mode 100644 index 000000000..1f2fde29b --- /dev/null +++ b/.github/workflows/hadolint.yml @@ -0,0 +1,56 @@ +name: hadolint.yml +on: + workflow_call: + inputs: + version: + description: "The version of the Posit Bakery tool to install" + default: "main" + required: false + type: string + hadolint-version: + description: "The hadolint release version to install (e.g., v2.12.0)" + default: "latest" + required: false + type: string + context: + description: "The Bakery context to use (directory)" + default: "." + required: false + type: string + +defaults: + run: + shell: bash + +# Security policy: No ${{ }} expressions in `run:` blocks. +# All expression values are assigned to `env:` and referenced as +# shell variables. This prevents script injection from runtime values +# (matrix outputs, secrets) and keeps the rule enforceable by zizmor +# without per-expression exceptions. + +jobs: + hadolint: + name: Hadolint + runs-on: "ubuntu-latest" + steps: + + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup bakery + uses: "posit-dev/images-shared/setup-bakery@main" + with: + version: ${{ inputs.version }} + + - name: Setup hadolint + uses: "posit-dev/images-shared/setup-hadolint@main" + with: + version: ${{ inputs.hadolint-version }} + base_path: ${{ inputs.context }} + + - name: Run hadolint + env: + CONTEXT: ${{ inputs.context }} + run: | + bakery hadolint run --matrix-versions include --dev-versions include \ + --context "$CONTEXT" diff --git a/.github/workflows/product-release.yml b/.github/workflows/product-release.yml index 57012d2da..5a6272427 100644 --- a/.github/workflows/product-release.yml +++ b/.github/workflows/product-release.yml @@ -19,6 +19,12 @@ on: description: "GitHub App private key for creating tokens" required: true +# Security policy: No ${{ }} expressions in `run:` blocks. +# All expression values are assigned to `env:` and referenced as +# shell variables. This prevents script injection from runtime values +# (matrix outputs, secrets) and keeps the rule enforceable by zizmor +# without per-expression exceptions. + jobs: release: runs-on: ubuntu-latest @@ -42,8 +48,10 @@ jobs: - name: Parse version id: parse + env: + INPUT_VERSION: ${{ inputs.version }} run: | - VERSION="${{ inputs.version }}" + VERSION="$INPUT_VERSION" DISPLAY_VERSION="${VERSION%%[+-]*}" EDITION="${DISPLAY_VERSION%.*}" @@ -133,6 +141,8 @@ jobs: IMAGES: ${{ inputs.images }} VERSION: ${{ steps.parse.outputs.version }} DISPLAY_VERSION: ${{ steps.parse.outputs.display_version }} + LATEST_CHANGED: ${{ steps.release.outputs.latest_changed }} + OLD_DISPLAY: ${{ steps.release.outputs.old_display_version }} run: | BRANCH="release/${DISPLAY_VERSION}" git fetch origin "$BRANCH" 2>/dev/null || true @@ -145,8 +155,8 @@ jobs: BODY="Updates images to version \`${VERSION}\`." BODY+=$'\n\n'"**Images:** $(echo $IMAGES | tr ' ' ', ')" - if [ "${{ steps.release.outputs.latest_changed }}" = "true" ]; then - BODY+=$'\n'"**Latest changed:** \`${{ steps.release.outputs.old_display_version }}\` → \`${DISPLAY_VERSION}\`" + if [ "$LATEST_CHANGED" = "true" ]; then + BODY+=$'\n'"**Latest changed:** \`${OLD_DISPLAY}\` → \`${DISPLAY_VERSION}\`" fi gh pr create \ From 94be2cecbded1a208e844248f1775e25bb8a0e57 Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Fri, 10 Apr 2026 11:34:29 -0500 Subject: [PATCH 03/15] Add permissions to shared reusable workflows Declare least-privilege permissions on all shared reusable workflows. Each workflow gets permissions: {} at the top level to drop all default permissions, then per-job permissions grant only what is needed. - bakery-build-native.yml: matrix {}, build-test and merge {contents: read, packages: write}, readme {contents: read} - bakery-build.yml: matrix {}, build {contents: read, packages: write}, readme {contents: read} - clean.yml: both jobs {contents: read, packages: write} - hadolint.yml: {contents: read} - product-release.yml: top-level {} (job-level already existed) --- .github/workflows/bakery-build-native.yml | 11 +++++++++++ .github/workflows/bakery-build.yml | 8 ++++++++ .github/workflows/clean.yml | 8 ++++++++ .github/workflows/hadolint.yml | 4 ++++ .github/workflows/product-release.yml | 2 ++ 5 files changed, 33 insertions(+) diff --git a/.github/workflows/bakery-build-native.yml b/.github/workflows/bakery-build-native.yml index 74575c028..12bbaeb9c 100644 --- a/.github/workflows/bakery-build-native.yml +++ b/.github/workflows/bakery-build-native.yml @@ -81,10 +81,13 @@ defaults: # (matrix outputs, secrets) and keeps the rule enforceable by zizmor # without per-expression exceptions. +permissions: {} + jobs: matrix: name: Image Matrix runs-on: ubuntu-latest + permissions: {} outputs: platform-matrix: ${{ steps.images-by-platform.outputs.platform_matrix }} versions-matrix: ${{ steps.images-by-version.outputs.versions_matrix }} @@ -117,6 +120,9 @@ jobs: build-test: name: "Build/Test ${{ matrix.img.image }}:${{ matrix.img.version }} (${{ matrix.img.platform }})" + permissions: + contents: read + packages: write needs: matrix strategy: fail-fast: false @@ -251,6 +257,9 @@ jobs: merge: name: "Merge/Push ${{ matrix.img.image }}:${{ matrix.img.version }}" + permissions: + contents: read + packages: write needs: - matrix - build-test @@ -348,6 +357,8 @@ jobs: readme: name: Push READMEs + permissions: + contents: read if: ${{ inputs.push }} needs: - merge diff --git a/.github/workflows/bakery-build.yml b/.github/workflows/bakery-build.yml index 76f5043da..0bc113a22 100644 --- a/.github/workflows/bakery-build.yml +++ b/.github/workflows/bakery-build.yml @@ -72,10 +72,13 @@ defaults: # (matrix outputs, secrets) and keeps the rule enforceable by zizmor # without per-expression exceptions. +permissions: {} + jobs: matrix: name: Image Matrix runs-on: ubuntu-latest + permissions: {} outputs: matrix: ${{ steps.images.outputs.matrix }} @@ -99,6 +102,9 @@ jobs: build: name: "${{ matrix.img.image }}:${{ matrix.img.version }}" + permissions: + contents: read + packages: write needs: matrix runs-on: ${{ inputs.runs-on }} strategy: @@ -222,6 +228,8 @@ jobs: readme: name: Push READMEs + permissions: + contents: read if: ${{ inputs.push }} needs: - build diff --git a/.github/workflows/clean.yml b/.github/workflows/clean.yml index 7ed47ca3a..4ef77afae 100644 --- a/.github/workflows/clean.yml +++ b/.github/workflows/clean.yml @@ -72,10 +72,15 @@ defaults: # (matrix outputs, secrets) and keeps the rule enforceable by zizmor # without per-expression exceptions. +permissions: {} + jobs: clean-caches: name: Clean Caches runs-on: "ubuntu-latest" + permissions: + contents: read + packages: write if: ${{ inputs.clean-caches == true }} steps: @@ -119,6 +124,9 @@ jobs: clean-temp: name: Clean Temporary Images runs-on: "ubuntu-latest" + permissions: + contents: read + packages: write if: ${{ inputs.clean-temporary-images == true }} steps: diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml index 1f2fde29b..703173ea9 100644 --- a/.github/workflows/hadolint.yml +++ b/.github/workflows/hadolint.yml @@ -28,10 +28,14 @@ defaults: # (matrix outputs, secrets) and keeps the rule enforceable by zizmor # without per-expression exceptions. +permissions: {} + jobs: hadolint: name: Hadolint runs-on: "ubuntu-latest" + permissions: + contents: read steps: - name: Checkout diff --git a/.github/workflows/product-release.yml b/.github/workflows/product-release.yml index 5a6272427..b7eebceef 100644 --- a/.github/workflows/product-release.yml +++ b/.github/workflows/product-release.yml @@ -25,6 +25,8 @@ on: # (matrix outputs, secrets) and keeps the rule enforceable by zizmor # without per-expression exceptions. +permissions: {} + jobs: release: runs-on: ubuntu-latest From f34e317aca400fdf2f101bd6fa5e519d3c32d7df Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Fri, 10 Apr 2026 11:35:07 -0500 Subject: [PATCH 04/15] Add permissions to ci.yml and issues.yml Declare least-privilege permissions for the CI and issue automation workflows. - ci.yml: top-level {}, ci meta-job {}, test {contents: read}, release {contents: write}. Bakery and clean jobs already had permissions declared. - issues.yml: top-level {} and job-level {} since the job uses a GitHub App token, not GITHUB_TOKEN --- .github/workflows/ci.yml | 6 ++++++ .github/workflows/issues.yml | 3 +++ 2 files changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 140b80f8b..740e27661 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,10 +12,12 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true +permissions: {} jobs: ci: name: CI + permissions: {} # This should be the only action checked as required in the repo settings. # # This is a meta-job, here to express the conditions we require @@ -37,6 +39,8 @@ jobs: test: name: Test + permissions: + contents: read runs-on: ubuntu-latest-8x steps: - name: Checkout @@ -163,6 +167,8 @@ jobs: release: name: Release/Snapshot + permissions: + contents: write if: (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.event_name == 'merge_group') needs: - test diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index 7d0a78246..e32df8a30 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -4,8 +4,11 @@ on: types: - opened +permissions: {} + jobs: issue: + permissions: {} # only run in posit-dev/images-shared. if: github.repository == 'posit-dev/images-shared' runs-on: ubuntu-latest From ea1020112566c0e1a2d4c8a8b7a3c2cd18918ce5 Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Fri, 10 Apr 2026 11:42:31 -0500 Subject: [PATCH 05/15] Fix missing permissions for checkout, OIDC, and checks Matrix jobs in bakery-build-native.yml and bakery-build.yml had permissions: {} but use actions/checkout, which requires contents: read on private repos. Build and merge jobs use aws-actions/configure-aws-credentials with OIDC role assumption, which requires id-token: write. Add the permission to build-test and merge in the native workflow and build in the QEMU workflow. The ci.yml test job uses publish-unit-test-result-action which needs checks: write to post results. --- .github/workflows/bakery-build-native.yml | 5 ++++- .github/workflows/bakery-build.yml | 4 +++- .github/workflows/ci.yml | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/bakery-build-native.yml b/.github/workflows/bakery-build-native.yml index 12bbaeb9c..552813c28 100644 --- a/.github/workflows/bakery-build-native.yml +++ b/.github/workflows/bakery-build-native.yml @@ -87,7 +87,8 @@ jobs: matrix: name: Image Matrix runs-on: ubuntu-latest - permissions: {} + permissions: + contents: read outputs: platform-matrix: ${{ steps.images-by-platform.outputs.platform_matrix }} versions-matrix: ${{ steps.images-by-version.outputs.versions_matrix }} @@ -123,6 +124,7 @@ jobs: permissions: contents: read packages: write + id-token: write needs: matrix strategy: fail-fast: false @@ -260,6 +262,7 @@ jobs: permissions: contents: read packages: write + id-token: write needs: - matrix - build-test diff --git a/.github/workflows/bakery-build.yml b/.github/workflows/bakery-build.yml index 0bc113a22..ab74af5d1 100644 --- a/.github/workflows/bakery-build.yml +++ b/.github/workflows/bakery-build.yml @@ -78,7 +78,8 @@ jobs: matrix: name: Image Matrix runs-on: ubuntu-latest - permissions: {} + permissions: + contents: read outputs: matrix: ${{ steps.images.outputs.matrix }} @@ -105,6 +106,7 @@ jobs: permissions: contents: read packages: write + id-token: write needs: matrix runs-on: ${{ inputs.runs-on }} strategy: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 740e27661..093b7a1d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,7 @@ jobs: name: Test permissions: contents: read + checks: write runs-on: ubuntu-latest-8x steps: - name: Checkout From 23c306910caccda152e4bce15df51d0369bf01ed Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Fri, 10 Apr 2026 14:12:12 -0500 Subject: [PATCH 06/15] Skip test result publish on Dependabot PRs Dependabot and fork PRs get a restricted GITHUB_TOKEN without checks:write permission. The publish-unit-test- result-action fails with 403 in that context. Skip it rather than switching to pull_request_target, which is unsafe for code checkout workflows. Tests still run and report pass/fail via job status. --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 093b7a1d3..11b8b96a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,7 +102,9 @@ jobs: uv build - name: Publish results - if: always() + # Dependabot and fork PRs get a restricted GITHUB_TOKEN without + # checks:write. Skip rather than switch to pull_request_target. + if: always() && github.actor != 'dependabot[bot]' uses: EnricoMi/publish-unit-test-result-action@v2 with: files: ./posit-bakery/results.xml From f6cac3f6a053c337f5c10bb6f91e87708463ba5c Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Fri, 10 Apr 2026 14:32:19 -0500 Subject: [PATCH 07/15] Also skip test result publish on fork PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fork PRs have the same restricted GITHUB_TOKEN as Dependabot PRs — checks:write is unavailable. Check both github.actor and the fork flag. --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8250a62d7..8c93bf426 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,7 +110,10 @@ jobs: - name: Publish results # Dependabot and fork PRs get a restricted GITHUB_TOKEN without # checks:write. Skip rather than switch to pull_request_target. - if: always() && github.actor != 'dependabot[bot]' + if: >- + always() + && github.actor != 'dependabot[bot]' + && github.event.pull_request.head.repo.fork != true uses: EnricoMi/publish-unit-test-result-action@v2 with: files: ./posit-bakery/results.xml From 36e4763586a82730cca60bc56080bb986119a0ab Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Fri, 10 Apr 2026 14:46:30 -0500 Subject: [PATCH 08/15] Remove top-level permissions from reusable workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reusable workflow (workflow_call) top-level permissions act as a ceiling — the caller can never grant more than what the reusable workflow declares. With permissions: {}, the caller's job-level permissions (contents:read, packages:write, etc.) are blocked entirely, causing a startup_failure. Job-level permissions within the reusable workflow are kept — they constrain what each job uses without blocking what the caller grants. --- .github/workflows/bakery-build-native.yml | 2 -- .github/workflows/bakery-build.yml | 2 -- .github/workflows/clean.yml | 2 -- .github/workflows/hadolint.yml | 2 -- .github/workflows/product-release.yml | 2 -- 5 files changed, 10 deletions(-) diff --git a/.github/workflows/bakery-build-native.yml b/.github/workflows/bakery-build-native.yml index 552813c28..7bacee947 100644 --- a/.github/workflows/bakery-build-native.yml +++ b/.github/workflows/bakery-build-native.yml @@ -81,8 +81,6 @@ defaults: # (matrix outputs, secrets) and keeps the rule enforceable by zizmor # without per-expression exceptions. -permissions: {} - jobs: matrix: name: Image Matrix diff --git a/.github/workflows/bakery-build.yml b/.github/workflows/bakery-build.yml index ab74af5d1..983d5e398 100644 --- a/.github/workflows/bakery-build.yml +++ b/.github/workflows/bakery-build.yml @@ -72,8 +72,6 @@ defaults: # (matrix outputs, secrets) and keeps the rule enforceable by zizmor # without per-expression exceptions. -permissions: {} - jobs: matrix: name: Image Matrix diff --git a/.github/workflows/clean.yml b/.github/workflows/clean.yml index 4ef77afae..e6abcd842 100644 --- a/.github/workflows/clean.yml +++ b/.github/workflows/clean.yml @@ -72,8 +72,6 @@ defaults: # (matrix outputs, secrets) and keeps the rule enforceable by zizmor # without per-expression exceptions. -permissions: {} - jobs: clean-caches: name: Clean Caches diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml index 703173ea9..98ce8056e 100644 --- a/.github/workflows/hadolint.yml +++ b/.github/workflows/hadolint.yml @@ -28,8 +28,6 @@ defaults: # (matrix outputs, secrets) and keeps the rule enforceable by zizmor # without per-expression exceptions. -permissions: {} - jobs: hadolint: name: Hadolint diff --git a/.github/workflows/product-release.yml b/.github/workflows/product-release.yml index b7eebceef..5a6272427 100644 --- a/.github/workflows/product-release.yml +++ b/.github/workflows/product-release.yml @@ -25,8 +25,6 @@ on: # (matrix outputs, secrets) and keeps the rule enforceable by zizmor # without per-expression exceptions. -permissions: {} - jobs: release: runs-on: ubuntu-latest From cb05dc7e2fed6bcf7e5fb898d8662c9f68b06d47 Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Fri, 10 Apr 2026 14:51:46 -0500 Subject: [PATCH 09/15] Remove top-level permissions from ci.yml Top-level permissions: {} on a workflow that calls reusable workflows may constrain the caller-to-callee permission grant, causing startup_failure. Remove it and rely on per-job permissions declarations instead. --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c93bf426..b389b4dd4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,8 +12,6 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true -permissions: {} - jobs: ci: name: CI From 62e5faebbc1e26e07fb62c8cec8e284e42369234 Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Fri, 10 Apr 2026 14:56:03 -0500 Subject: [PATCH 10/15] Remove id-token:write from reusable workflow jobs Job-level permissions in a reusable workflow cannot exceed what the caller grants. The caller (ci.yml) grants contents:read + packages:write but not id-token:write, causing startup_failure when the callee's job declares it. The id-token:write permission for AWS OIDC must be granted by the caller, not declared inside the reusable workflow. --- .github/workflows/bakery-build-native.yml | 2 -- .github/workflows/bakery-build.yml | 1 - 2 files changed, 3 deletions(-) diff --git a/.github/workflows/bakery-build-native.yml b/.github/workflows/bakery-build-native.yml index 7bacee947..2cc7a83c0 100644 --- a/.github/workflows/bakery-build-native.yml +++ b/.github/workflows/bakery-build-native.yml @@ -122,7 +122,6 @@ jobs: permissions: contents: read packages: write - id-token: write needs: matrix strategy: fail-fast: false @@ -260,7 +259,6 @@ jobs: permissions: contents: read packages: write - id-token: write needs: - matrix - build-test diff --git a/.github/workflows/bakery-build.yml b/.github/workflows/bakery-build.yml index 983d5e398..a31be6cd2 100644 --- a/.github/workflows/bakery-build.yml +++ b/.github/workflows/bakery-build.yml @@ -104,7 +104,6 @@ jobs: permissions: contents: read packages: write - id-token: write needs: matrix runs-on: ${{ inputs.runs-on }} strategy: From e0e838d4522f99943b4155cc619b593cd3e05d4e Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Fri, 10 Apr 2026 15:10:47 -0500 Subject: [PATCH 11/15] Add pull-requests:write for test result comments The publish-unit-test-result-action posts a comment on the PR which requires pull-requests:write in addition to checks:write. --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b389b4dd4..7e8d86211 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,7 @@ jobs: permissions: contents: read checks: write + pull-requests: write runs-on: ubuntu-latest-8x steps: - name: Checkout From 6c2e23bf46ed18c0322694627a209354a7702518 Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Fri, 10 Apr 2026 11:57:28 -0500 Subject: [PATCH 12/15] Add fork-safe PR build workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New bakery-build-pr.yml shared workflow for PR builds that works safely with fork PRs. Key differences from the native build workflow: - Detects fork PRs and conditionally skips GHCR login/cache - No push, no merge, no readme jobs - Skips arm64 builds for fork PRs (paid runners unavailable) - No secrets section — uses only inherited GITHUB_TOKEN - All expressions in env: blocks, never in run: blocks --- .github/workflows/bakery-build-pr.yml | 216 ++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 .github/workflows/bakery-build-pr.yml diff --git a/.github/workflows/bakery-build-pr.yml b/.github/workflows/bakery-build-pr.yml new file mode 100644 index 000000000..e5882bfb1 --- /dev/null +++ b/.github/workflows/bakery-build-pr.yml @@ -0,0 +1,216 @@ +# Fork-safe PR build workflow for Posit container images. +# +# Unlike bakery-build-native.yml this workflow never pushes images. +# Fork PRs get a read-only GITHUB_TOKEN with no access to repo secrets, +# so registry logins and caching are conditional on the PR source. +# +# Security policy: No ${{ }} expressions in `run:` blocks. +# All expression values are assigned to `env:` and referenced as +# shell variables. This prevents script injection from runtime values +# (matrix outputs, secrets) and keeps the rule enforceable by zizmor +# without per-expression exceptions. + +name: Bakery PR Build + +on: + workflow_call: + inputs: + version: + description: "Bakery version to install" + default: "main" + required: false + type: string + context: + description: "Path to the bakery context" + default: "." + required: false + type: string + dev-versions: + description: "Whether to include development versions [default: exclude]" + default: "exclude" + required: false + type: string + matrix-versions: + description: "Whether to include matrix versions [default: exclude]" + default: "exclude" + required: false + type: string + retry: + description: "Number of times to retry a failed build" + default: 1 + required: false + type: number + amd64-builder: + description: "Runner label for amd64 builds" + default: "ubuntu-latest-4x" + required: false + type: string + arm64-builder: + description: "Runner label for arm64 builds" + default: "ubuntu-24.04-arm64-4-core" + required: false + type: string + # NO secrets section — only inherited GITHUB_TOKEN + +permissions: {} + +defaults: + run: + shell: bash + +jobs: + detect: + name: Detect Fork + runs-on: ubuntu-latest + permissions: {} + outputs: + is-fork: ${{ steps.check.outputs.is-fork }} + steps: + - name: Check fork status + id: check + env: + IS_FORK: ${{ github.event.pull_request.head.repo.fork == true }} + run: echo "is-fork=$IS_FORK" >> "$GITHUB_OUTPUT" + + matrix: + name: Image Matrix + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + platform-matrix: ${{ steps.images-by-platform.outputs.platform_matrix }} + versions-matrix: ${{ steps.images-by-version.outputs.versions_matrix }} + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install + uses: "posit-dev/images-shared/setup-bakery@main" + with: + version: ${{ inputs.version }} + + - name: Images by Version/Platform + id: images-by-platform + env: + DEV_VERSIONS: ${{ inputs.dev-versions }} + MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + BAKERY_CONTEXT: ${{ inputs.context }} + run: | + echo "platform_matrix=$(bakery ci matrix --quiet --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS" --context "$BAKERY_CONTEXT" | jq --compact-output .)" >> "$GITHUB_OUTPUT" + + - name: Images by Version + id: images-by-version + env: + DEV_VERSIONS: ${{ inputs.dev-versions }} + MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + BAKERY_CONTEXT: ${{ inputs.context }} + run: | + echo "versions_matrix=$(bakery ci matrix --quiet --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS" --exclude platform --context "$BAKERY_CONTEXT" | jq --compact-output .)" >> "$GITHUB_OUTPUT" + + build-test: + name: "Build/Test ${{ matrix.img.image }}:${{ matrix.img.version }} (${{ matrix.img.platform }})" + needs: + - detect + - matrix + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + img: ${{ fromJson(needs.matrix.outputs.platform-matrix) }} + runs-on: ${{ matrix.img.platform == 'linux/arm64' && inputs.arm64-builder || inputs.amd64-builder }} + # Skip arm64 for fork PRs — paid runners may not be available + if: needs.detect.outputs.is-fork != 'true' || matrix.img.platform != 'linux/arm64' + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup bakery + uses: "posit-dev/images-shared/setup-bakery@main" + with: + version: ${{ inputs.version }} + + - name: Setup goss + uses: "posit-dev/images-shared/setup-goss@main" + + - name: Set up Docker + uses: docker/setup-docker-action@v5 + with: + daemon-config: | + { + "features": { + "containerd-snapshotter": true + } + } + + - name: Setup docker buildx + uses: docker/setup-buildx-action@v4 + + - name: Setup ORAS CLI + uses: oras-project/setup-oras@v1 + + - name: Login to GitHub Container Registry + if: needs.detect.outputs.is-fork != 'true' + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Normalize platform + id: normalize-platform + env: + BUILD_PLATFORM: ${{ matrix.img.platform }} + run: | + PLATFORM=${BUILD_PLATFORM#linux/} + echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + + - name: Build + env: + IS_FORK: ${{ needs.detect.outputs.is-fork }} + GIT_SHA: ${{ github.sha }} + RETRY: ${{ inputs.retry }} + IMAGE_NAME: ${{ matrix.img.image }} + IMAGE_VERSION: ${{ matrix.img.version }} + IMAGE_PLATFORM: ${{ matrix.img.platform }} + DEV_VERSIONS: ${{ inputs.dev-versions }} + MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + BAKERY_CONTEXT: ${{ inputs.context }} + REGISTRY_OWNER: ${{ github.repository_owner }} + run: | + CACHE_FLAGS="" + if [ "$IS_FORK" != "true" ]; then + CACHE_FLAGS="--cache-registry ghcr.io/${REGISTRY_OWNER}" + fi + bakery build \ + --strategy build --pull --load \ + --retry "$RETRY" \ + --image-name "^${IMAGE_NAME}$" \ + --image-version "$IMAGE_VERSION" \ + --image-platform "$IMAGE_PLATFORM" \ + --dev-versions "$DEV_VERSIONS" \ + --matrix-versions "$MATRIX_VERSIONS" \ + $CACHE_FLAGS \ + --context "$BAKERY_CONTEXT" + + - name: Test + env: + IMAGE_NAME: ${{ matrix.img.image }} + IMAGE_VERSION: ${{ matrix.img.version }} + IMAGE_PLATFORM: ${{ matrix.img.platform }} + DEV_VERSIONS: ${{ inputs.dev-versions }} + MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + BAKERY_CONTEXT: ${{ inputs.context }} + run: | + 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" \ + --context "$BAKERY_CONTEXT" From 65de5bb003bbe16453695594ca9e39a702097b3d Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Fri, 10 Apr 2026 11:57:47 -0500 Subject: [PATCH 13/15] Wire bakery-build-pr.yml into CI Add bakery-pr job to ci.yml that exercises the new fork-safe PR workflow on pull_request events. Added to allowed-skips since it only runs on PRs (not push/merge_group). --- .github/workflows/ci.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e8d86211..f67b44878 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,11 +28,13 @@ jobs: - test - bakery - bakery-native + - bakery-pr - release steps: - uses: re-actors/alls-green@release/v1 with: + allowed-skips: bakery-pr jobs: ${{ toJSON(needs) }} test: @@ -141,6 +143,19 @@ jobs: context: "./posit-bakery/test/resources/multiplatform/" dev-versions: include + bakery-pr: + name: Bakery PR Build + if: github.event_name == 'pull_request' + permissions: + contents: read + packages: write + + uses: "./.github/workflows/bakery-build-pr.yml" + with: + version: ${{ github.head_ref || github.ref_name }} + context: "./posit-bakery/test/resources/multiplatform/" + dev-versions: include + with-macros-clean-caches: name: Clean Caches (with-macros suite) permissions: From 4cd1c7a08bc55e55ee32789de7bb18963f949c19 Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Fri, 10 Apr 2026 12:03:22 -0500 Subject: [PATCH 14/15] Use default bakery version for PR builds The head_ref pattern used by other CI jobs breaks on fork PRs because the fork's branch doesn't exist in the base repo. The PR build job uses the default "main" version instead, which always resolves. --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f67b44878..5f14fa1ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -152,7 +152,9 @@ jobs: uses: "./.github/workflows/bakery-build-pr.yml" with: - version: ${{ github.head_ref || github.ref_name }} + # Don't pass version — default "main" is correct here. The head_ref + # pattern used by other jobs breaks on fork PRs because the fork's + # branch doesn't exist in posit-dev/images-shared. context: "./posit-bakery/test/resources/multiplatform/" dev-versions: include From 5a076248056b087cb586c54f13ed0026f549b075 Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Fri, 10 Apr 2026 14:54:20 -0500 Subject: [PATCH 15/15] Remove top-level permissions from PR workflow Top-level permissions on workflow_call workflows act as a ceiling that blocks caller-granted permissions, causing startup_failure. Use per-job permissions only. --- .github/workflows/bakery-build-pr.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/bakery-build-pr.yml b/.github/workflows/bakery-build-pr.yml index e5882bfb1..6c116714e 100644 --- a/.github/workflows/bakery-build-pr.yml +++ b/.github/workflows/bakery-build-pr.yml @@ -52,8 +52,6 @@ on: type: string # NO secrets section — only inherited GITHUB_TOKEN -permissions: {} - defaults: run: shell: bash