diff --git a/template/.github/workflows/build.yaml.j2 b/template/.github/workflows/build.yaml.j2 index ac52870f..0b7fa7b0 100644 --- a/template/.github/workflows/build.yaml.j2 +++ b/template/.github/workflows/build.yaml.j2 @@ -24,7 +24,6 @@ on: env: OPERATOR_NAME: "{[ operator.name }]" RUST_NIGHTLY_TOOLCHAIN_VERSION: "{[ rust_nightly_version }]" - NIX_PKG_MANAGER_VERSION: "{[ nix_pkg_manager_version }]" RUST_TOOLCHAIN_VERSION: "{[ rust_version }]" HADOLINT_VERSION: "{[ hadolint_version }]" PYTHON_VERSION: "{[ python_version }]" @@ -98,35 +97,23 @@ jobs: - name: Run cargo-udeps run: cargo udeps --workspace --all-targets - build-container-image: - name: Build/Publish ${{ matrix.runner.arch }} Image + operator-version: + name: Determine Operator Version if: (github.event_name != 'merge_group') && needs.detect-changes.outputs.detected == 'true' needs: [detect-changes] + runs-on: ubuntu-latest permissions: - id-token: write - strategy: - fail-fast: false - matrix: - runner: - - { name: "ubuntu-latest", arch: "amd64" } - - { name: "ubicloud-standard-8-arm", arch: "arm64" } - runs-on: ${{ matrix.runner.name }} + contents: read outputs: operator-version: ${{ steps.version.outputs.OPERATOR_VERSION }} steps: - - name: Install host dependencies - uses: awalsh128/cache-apt-pkgs-action@2c09a5e66da6c8016428a2172bd76e5e4f14bb17 # v1.5.3 - with: - packages: protobuf-compiler krb5-user libkrb5-dev libclang-dev liblzma-dev libssl-dev pkg-config apt-transport-https - version: ${{ matrix.runner.name }} - - name: Checkout Repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false submodules: recursive - - name: Update/Extract Operator Version + - name: Extract Operator Version id: version env: PR_BASE_REF: ${{ github.event.pull_request.base.ref }} @@ -138,6 +125,8 @@ jobs: set -euo pipefail [ -n "$GITHUB_DEBUG" ] && set -x + # `cargo metadata --no-deps` only parses the manifests, so the + # toolchain preinstalled on the runner is sufficient here. CURRENT_VERSION=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.name == "stackable-{[ operator.name }]") | .version') if [ "$GITHUB_EVENT_NAME" == 'pull_request' ]; then @@ -152,153 +141,62 @@ jobs: NEW_VERSION="$CURRENT_VERSION" fi - sed -i "s/version = \"${CURRENT_VERSION}\"/version = \"${NEW_VERSION}\"/" Cargo.toml + # The version is only reported here. Stamping it into Cargo.toml + # happens inside the builder workflow via its cargo-workspace-version + # input, because caller-supplied shell must not run on the builder's + # runners. echo "OPERATOR_VERSION=$NEW_VERSION" | tee -a "$GITHUB_OUTPUT" - - name: Install Nix - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 - - - name: Install Rust ${{ env.RUST_TOOLCHAIN_VERSION }} Toolchain - shell: bash - run: rustup toolchain install "${RUST_TOOLCHAIN_VERSION}" - - - name: Build Container Image - id: build - uses: stackabletech/actions/build-container-image@dc83bb926cc464f0f32454e934777116bd1c7768 # v0.16.3 - with: - image-name: ${{ env.OPERATOR_NAME }} - image-index-manifest-tag: ${{ steps.version.outputs.OPERATOR_VERSION }} - build-arguments: VERSION=${{ steps.version.outputs.OPERATOR_VERSION }} - container-file: docker/Dockerfile - - - name: Publish Container Image to oci.stackable.tech - if: ${{ !github.event.pull_request.head.repo.fork }} - uses: stackabletech/actions/publish-image@dc83bb926cc464f0f32454e934777116bd1c7768 # v0.16.3 - with: - image-registry-uri: oci.stackable.tech - image-registry-username: robot$sdp+github-action-build - image-registry-password: ${{ secrets.HARBOR_ROBOT_SDP_GITHUB_ACTION_BUILD_SECRET }} - image-repository: sdp/${{ env.OPERATOR_NAME }} - image-manifest-tag: ${{ steps.build.outputs.image-manifest-tag }} - source-image-uri: ${{ steps.build.outputs.image-manifest-uri }} - - - name: Publish Container Image to quay.io - if: ${{ !github.event.pull_request.head.repo.fork }} - uses: stackabletech/actions/publish-image@dc83bb926cc464f0f32454e934777116bd1c7768 # v0.16.3 - with: - image-registry-uri: quay.io - image-registry-username: stackable+robot_sdp_github_action_build - image-registry-password: ${{ secrets.QUAY_ROBOT_SDP_GITHUB_ACTION_BUILD_SECRET }} - image-repository: stackable/sdp/${{ env.OPERATOR_NAME }} - image-manifest-tag: ${{ steps.build.outputs.image-manifest-tag }} - source-image-uri: ${{ steps.build.outputs.image-manifest-uri }} - - publish-index-manifest: - name: Publish/Sign ${{ needs.build-container-image.outputs.operator-version }} Index - if: | - (github.event_name != 'merge_group') - && needs.detect-changes.outputs.detected == 'true' - && !github.event.pull_request.head.repo.fork - needs: - - detect-changes - - build-container-image - permissions: - id-token: write - runs-on: ubuntu-latest - outputs: - oci-index-digest: ${{ steps.publish-oci.outputs.image-index-manifest-digest }} - quay-index-digest: ${{ steps.publish-quay.outputs.image-index-manifest-digest }} - steps: - - name: Checkout Repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - persist-credentials: false - - - name: Publish and Sign Image Index to oci.stackable.tech - id: publish-oci - uses: stackabletech/actions/publish-image-index-manifest@dc83bb926cc464f0f32454e934777116bd1c7768 # v0.16.3 - with: - image-registry-uri: oci.stackable.tech - image-registry-username: robot$sdp+github-action-build - image-registry-password: ${{ secrets.HARBOR_ROBOT_SDP_GITHUB_ACTION_BUILD_SECRET }} - image-repository: sdp/${{ env.OPERATOR_NAME }} - image-index-manifest-tag: ${{ needs.build-container-image.outputs.operator-version }} - - - name: Publish and Sign Image Index to quay.io - id: publish-quay - uses: stackabletech/actions/publish-image-index-manifest@dc83bb926cc464f0f32454e934777116bd1c7768 # v0.16.3 - with: - image-registry-uri: quay.io - image-registry-username: stackable+robot_sdp_github_action_build - image-registry-password: ${{ secrets.QUAY_ROBOT_SDP_GITHUB_ACTION_BUILD_SECRET }} - image-repository: stackable/sdp/${{ env.OPERATOR_NAME }} - image-index-manifest-tag: ${{ needs.build-container-image.outputs.operator-version }} - - # Generate SLSA build provenance for the multi-arch image index and attach it - # to the published image in each registry. The reusable workflow signs the - # provenance with keyless signing (GitHub Actions as the OIDC identity) and - # pushes the attestation next to the image. - provenance-oci: - name: Generate Provenance for ${{ needs.build-container-image.outputs.operator-version }} (oci.stackable.tech) - if: | - (github.event_name != 'merge_group') - && needs.detect-changes.outputs.detected == 'true' - && !github.event.pull_request.head.repo.fork - needs: - - detect-changes - - build-container-image - - publish-index-manifest - permissions: - actions: read # detect the build workflow that generated the image - id-token: write # mint the OIDC token for keyless signing - packages: write # needed until https://github.com/slsa-framework/slsa-github-generator/issues/1257 is resolved - # MUST be referenced by a @vX.Y.Z tag (not a SHA), otherwise the reusable - # workflow cannot verify its own provenance. - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 - with: - # The 'env' context is not available in job-level 'with' inputs of - # reusable workflow calls (unlike step-level 'with'), so OPERATOR_NAME - # can't be used here and the operator name is templated in directly. - image: oci.stackable.tech/sdp/{[ operator.name }] - digest: ${{ needs.publish-index-manifest.outputs.oci-index-digest }} - registry-username: robot$sdp+github-action-build - secrets: - registry-password: ${{ secrets.HARBOR_ROBOT_SDP_GITHUB_ACTION_BUILD_SECRET }} - - provenance-quay: - name: Generate Provenance for ${{ needs.build-container-image.outputs.operator-version }} (quay.io) - if: | - (github.event_name != 'merge_group') - && needs.detect-changes.outputs.detected == 'true' - && !github.event.pull_request.head.repo.fork - needs: - - detect-changes - - build-container-image - - publish-index-manifest + # Building, publishing, signing and attesting all happen inside the shared + # builder workflow in stackabletech/actions. Running the build in a reusable + # workflow separate from this one is GitHub's documented mechanism for + # reaching SLSA v1.0 Build Level 3. + # + # Only source-level build parameters are passed, so this workflow cannot hand + # the builder a digest and obtain provenance for an image it did not build. + build-container-image: + name: Build/Publish/Attest ${{ needs.operator-version.outputs.operator-version }} Image + if: (github.event_name != 'merge_group') && needs.detect-changes.outputs.detected == 'true' + needs: [detect-changes, operator-version] permissions: - actions: read # detect the build workflow that generated the image - id-token: write # mint the OIDC token for keyless signing - packages: write # needed until https://github.com/slsa-framework/slsa-github-generator/issues/1257 is resolved - # MUST be referenced by a @vX.Y.Z tag (not a SHA), otherwise the reusable - # workflow cannot verify its own provenance. - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 + contents: read + id-token: write # cosign keyless signing and attestation OIDC identity + attestations: write # persist the attestation in the GitHub attestation store + artifact-metadata: write # create the artifact metadata storage record + # TODO: pin to a release tag once stackabletech/actions cuts one containing + # reusable_build_container_image.yaml + uses: stackabletech/actions/.github/workflows/reusable_build_container_image.yaml@main with: # The 'env' context is not available in job-level 'with' inputs of # reusable workflow calls (unlike step-level 'with'), so OPERATOR_NAME # can't be used here and the operator name is templated in directly. - image: quay.io/stackable/sdp/{[ operator.name }] - digest: ${{ needs.publish-index-manifest.outputs.quay-index-digest }} - registry-username: stackable+robot_sdp_github_action_build + image-name: "{[ operator.name }]" + image-index-manifest-tag: ${{ needs.operator-version.outputs.operator-version }} + container-file: docker/Dockerfile + build-arguments: VERSION=${{ needs.operator-version.outputs.operator-version }} + cargo-workspace-version: ${{ needs.operator-version.outputs.operator-version }} + arm64-runner: ubicloud-standard-8-arm + oci-repository: sdp/{[ operator.name }] + oci-username: robot$sdp+github-action-build + quay-repository: stackable/sdp/{[ operator.name }] + quay-username: stackable+robot_sdp_github_action_build + publish: ${{ !github.event.pull_request.head.repo.fork }} secrets: - registry-password: ${{ secrets.QUAY_ROBOT_SDP_GITHUB_ACTION_BUILD_SECRET }} + oci-password: ${{ secrets.HARBOR_ROBOT_SDP_GITHUB_ACTION_BUILD_SECRET }} + quay-password: ${{ secrets.QUAY_ROBOT_SDP_GITHUB_ACTION_BUILD_SECRET }} publish-helm-chart: - name: Package/Publish ${{ needs.build-container-image.outputs.operator-version }} Helm Chart + name: Package/Publish ${{ needs.operator-version.outputs.operator-version }} Helm Chart if: | (github.event_name != 'merge_group') && needs.detect-changes.outputs.detected == 'true' + # build-container-image is a dependency even though no output of it is used + # here: the chart's appVersion points at the image tag, so publishing a + # chart for an image that failed to build would advertise something that + # does not exist. needs: - detect-changes + - operator-version - build-container-image permissions: id-token: write @@ -318,8 +216,8 @@ jobs: chart-registry-password: ${{ secrets.HARBOR_ROBOT_SDP_CHARTS_GITHUB_ACTION_BUILD_SECRET }} chart-repository: sdp-charts chart-directory: deploy/helm/${{ env.OPERATOR_NAME }} - chart-version: ${{ needs.build-container-image.outputs.operator-version }} - app-version: ${{ needs.build-container-image.outputs.operator-version }} + chart-version: ${{ needs.operator-version.outputs.operator-version }} + app-version: ${{ needs.operator-version.outputs.operator-version }} publish-and-sign: ${{ !github.event.pull_request.head.repo.fork }} - name: Package, Publish, and Sign Helm Chart to quay.io @@ -330,21 +228,21 @@ jobs: chart-registry-password: ${{ secrets.QUAY_ROBOT_SDP_CHARTS_GITHUB_ACTION_BUILD_SECRET }} chart-repository: stackable/sdp-charts chart-directory: deploy/helm/${{ env.OPERATOR_NAME }} - chart-version: ${{ needs.build-container-image.outputs.operator-version }} - app-version: ${{ needs.build-container-image.outputs.operator-version }} + chart-version: ${{ needs.operator-version.outputs.operator-version }} + app-version: ${{ needs.operator-version.outputs.operator-version }} publish-and-sign: ${{ !github.event.pull_request.head.repo.fork }} helm-version: v3.17.4 # This is currently the latest version which supports pushing to quay.io openshift-preflight-check: - name: Run OpenShift Preflight Check for ${{ needs.build-container-image.outputs.operator-version }}-${{ matrix.arch }} + name: Run OpenShift Preflight Check for ${{ needs.operator-version.outputs.operator-version }}-${{ matrix.arch }} if: | (github.event_name != 'merge_group') && needs.detect-changes.outputs.detected == 'true' && !github.event.pull_request.head.repo.fork needs: - detect-changes + - operator-version - build-container-image - - publish-index-manifest strategy: fail-fast: false matrix: @@ -356,13 +254,13 @@ jobs: - name: Run OpenShift Preflight Check for oci.stackable.tech uses: stackabletech/actions/run-openshift-preflight@dc83bb926cc464f0f32454e934777116bd1c7768 # v0.16.3 with: - image-index-uri: oci.stackable.tech/sdp/${{ env.OPERATOR_NAME }}:${{ needs.build-container-image.outputs.operator-version }} + image-index-uri: oci.stackable.tech/sdp/${{ env.OPERATOR_NAME }}:${{ needs.operator-version.outputs.operator-version }} image-architecture: ${{ matrix.arch }} - name: Run OpenShift Preflight Check for quay.io uses: stackabletech/actions/run-openshift-preflight@dc83bb926cc464f0f32454e934777116bd1c7768 # v0.16.3 with: - image-index-uri: quay.io/stackable/sdp/${{ env.OPERATOR_NAME }}:${{ needs.build-container-image.outputs.operator-version }} + image-index-uri: quay.io/stackable/sdp/${{ env.OPERATOR_NAME }}:${{ needs.operator-version.outputs.operator-version }} image-architecture: ${{ matrix.arch }} # This job is a required check in GitHub Settings for this repository. @@ -377,17 +275,15 @@ jobs: # would *skip* this job, and GitHub treats a skipped required check as # passing - making a broken build mergeable. if: always() - # List every leaf job directly. A transitive failure (e.g. a failed - # publish-index-manifest that skips openshift-preflight-check) does not + # List every job directly. A transitive failure (e.g. a failed + # build-container-image that skips openshift-preflight-check) does not # surface as `failure` in `needs.*.result` unless the failing job is a # direct dependency. needs: - detect-changes - cargo-udeps + - operator-version - build-container-image - - publish-index-manifest - - provenance-oci - - provenance-quay - publish-helm-chart - openshift-preflight-check runs-on: ubuntu-latest @@ -418,9 +314,6 @@ jobs: needs: - detect-changes - build-container-image - - publish-index-manifest - - provenance-oci - - provenance-quay - publish-helm-chart runs-on: ubuntu-latest steps: @@ -433,17 +326,12 @@ jobs: uses: stackabletech/actions/send-slack-notification@dc83bb926cc464f0f32454e934777116bd1c7768 # v0.16.3 with: publish-helm-chart-result: ${{ needs.publish-helm-chart.result }} - publish-manifests-result: ${{ needs.publish-index-manifest.result }} + # Building, publishing the index manifest and generating provenance + # all happen inside the builder workflow, so the three notification + # fields collapse into its single job result. + publish-manifests-result: ${{ needs.build-container-image.result }} build-result: ${{ needs.build-container-image.result }} - # Provenance is generated per registry, but the notification only has a - # single field for it, so the two results are collapsed into the worst - # one. 'failure' must be reported verbatim, otherwise the notification - # is not marked as failed. - generate-provenance-result: >- - ${{ (needs.provenance-oci.result == 'failure' || needs.provenance-quay.result == 'failure') && 'failure' - || (needs.provenance-oci.result == 'cancelled' || needs.provenance-quay.result == 'cancelled') && 'cancelled' - || (needs.provenance-oci.result == 'skipped' || needs.provenance-quay.result == 'skipped') && 'skipped' - || 'success' }} + generate-provenance-result: ${{ needs.build-container-image.result }} slack-token: ${{ secrets.SLACK_CONTAINER_IMAGE_TOKEN }} channel-id: C07UG6JH44F # notifications-container-images type: container-image-build