diff --git a/.github/workflows/bakery-build-native.yml b/.github/workflows/bakery-build-native.yml index 3c1942aa..2cc7a83c 100644 --- a/.github/workflows/bakery-build-native.yml +++ b/.github/workflows/bakery-build-native.yml @@ -75,10 +75,18 @@ 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 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 }} @@ -94,15 +102,26 @@ 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 }})" + permissions: + contents: read + packages: write needs: matrix strategy: fail-fast: false @@ -141,18 +160,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 +203,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: @@ -228,6 +256,9 @@ jobs: merge: name: "Merge/Push ${{ matrix.img.image }}:${{ matrix.img.version }}" + permissions: + contents: read + packages: write needs: - matrix - build-test @@ -260,18 +291,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,15 +343,21 @@ 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: name: Push READMEs + permissions: + contents: read if: ${{ inputs.push }} needs: - merge @@ -345,8 +376,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-pr.yml b/.github/workflows/bakery-build-pr.yml new file mode 100644 index 00000000..6c116714 --- /dev/null +++ b/.github/workflows/bakery-build-pr.yml @@ -0,0 +1,214 @@ +# 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 + +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" diff --git a/.github/workflows/bakery-build.yml b/.github/workflows/bakery-build.yml index c040be02..a31be6cd 100644 --- a/.github/workflows/bakery-build.yml +++ b/.github/workflows/bakery-build.yml @@ -66,10 +66,18 @@ 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 runs-on: ubuntu-latest + permissions: + contents: read outputs: matrix: ${{ steps.images.outputs.matrix }} @@ -84,11 +92,18 @@ 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 }}" + permissions: + contents: read + packages: write needs: matrix runs-on: ${{ inputs.runs-on }} strategy: @@ -115,18 +130,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 +169,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,17 +210,25 @@ 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 + permissions: + contents: read if: ${{ inputs.push }} needs: - build @@ -217,8 +247,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/ci.yml b/.github/workflows/ci.yml index 9fd5d06a..5f14fa1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,10 +12,10 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true - 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 @@ -28,15 +28,21 @@ 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: name: Test + permissions: + contents: read + checks: write + pull-requests: write runs-on: ubuntu-latest-8x steps: - name: Checkout @@ -103,7 +109,12 @@ 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]' + && github.event.pull_request.head.repo.fork != true uses: EnricoMi/publish-unit-test-result-action@v2 with: files: ./posit-bakery/results.xml @@ -132,6 +143,21 @@ 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: + # 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 + with-macros-clean-caches: name: Clean Caches (with-macros suite) permissions: @@ -169,6 +195,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 @@ -211,10 +239,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 b38a7576..e6abcd84 100644 --- a/.github/workflows/clean.yml +++ b/.github/workflows/clean.yml @@ -66,10 +66,19 @@ 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 runs-on: "ubuntu-latest" + permissions: + contents: read + packages: write if: ${{ inputs.clean-caches == true }} steps: @@ -91,18 +100,31 @@ 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 runs-on: "ubuntu-latest" + permissions: + contents: read + packages: write if: ${{ inputs.clean-temporary-images == true }} steps: @@ -124,11 +146,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 index 56865fcf..98ce8056 100644 --- a/.github/workflows/hadolint.yml +++ b/.github/workflows/hadolint.yml @@ -22,10 +22,18 @@ 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" + permissions: + contents: read steps: - name: Checkout @@ -43,6 +51,8 @@ jobs: base_path: ${{ inputs.context }} - name: Run hadolint + env: + CONTEXT: ${{ inputs.context }} run: | bakery hadolint run --matrix-versions include --dev-versions include \ - --context ${{ inputs.context }} \ No newline at end of file + --context "$CONTEXT" diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index 7d0a7824..e32df8a3 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 diff --git a/.github/workflows/product-release.yml b/.github/workflows/product-release.yml index 57012d2d..5a627242 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 \