action #21
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: build-test-sign-image | |
| on: | |
| push: | |
| branches: [main, master] | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| # Needed for pushing to GHCR + keyless cosign signing (OIDC) | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write | |
| env: | |
| REGISTRY: ghcr.io | |
| jobs: | |
| build_test_and_release: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| # Docker repository names must be lowercase | |
| - name: Set image name (lowercase) | |
| id: image | |
| run: echo "name=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT | |
| - name: Set up QEMU (optional) | |
| uses: docker/setup-qemu-action@v3 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| # Log in before any buildx pull/push (buildx uses host Docker config). | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| logout: false | |
| - name: Log in to Azure CR (pull base image) | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: executionengine.azurecr.io | |
| username: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} | |
| password: ${{ secrets.CONTAINER_REGISTRY_TOKEN }} | |
| logout: false | |
| # From here on, buildx (and docker) can pull/push from/to GHCR and Azure. | |
| # Try to pull base image; only build if pull fails (e.g. first run or base not yet published). | |
| - name: Try pull base image | |
| id: pull_base | |
| run: | | |
| BASE_IMAGE="executionengine.azurecr.io/${{ steps.image.outputs.name }}-base:latest" | |
| if docker pull "$BASE_IMAGE"; then | |
| docker tag "$BASE_IMAGE" examplestudy-base:latest | |
| echo "need_build=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "need_build=true" >> $GITHUB_OUTPUT | |
| fi | |
| # Build base image only when it could not be pulled. | |
| # Multi-platform: build for linux/amd64 and linux/arm64 (Apple Silicon, ARM servers). | |
| # Note: Dockerfile.base has x86_64-only bits (Snowflake/MS ODBC); arm64 may need conditional steps. | |
| - name: Build base image (CI / local load) | |
| if: steps.pull_base.outputs.need_build == 'true' | |
| run: | | |
| BASE_REPO="executionengine.azurecr.io/${{ steps.image.outputs.name }}-base:latest" | |
| docker buildx build \ | |
| --file ./Dockerfile.base \ | |
| --tag "$BASE_REPO" \ | |
| --cache-from type=gha \ | |
| --cache-to type=gha,mode=max \ | |
| --platform linux/amd64,linux/arm64 \ | |
| --push \ | |
| . | |
| docker pull "$BASE_REPO" | |
| docker tag "$BASE_REPO" examplestudy-base:latest | |
| # Produces tags + labels (commit SHA, semver if you use tags, etc.) | |
| - name: Docker metadata | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }} | |
| tags: | | |
| type=sha,format=long | |
| type=ref,event=branch | |
| type=ref,event=tag | |
| labels: | | |
| org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} | |
| org.opencontainers.image.revision=${{ github.sha }} | |
| # Use plain docker build so FROM uses the host's base image (already pulled or built above). | |
| - name: Build study image (CI / local load) | |
| id: build_ci | |
| run: | | |
| set -o pipefail | |
| docker build \ | |
| --progress=plain \ | |
| --file ./Dockerfile \ | |
| --build-arg BASE_IMAGE=examplestudy-base:latest \ | |
| --tag ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}:ci-${{ github.sha }} \ | |
| --label "org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}" \ | |
| --label "org.opencontainers.image.revision=${{ github.sha }}" \ | |
| . 2>&1 | tee build.log | |
| - name: Upload build log | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: docker-build-log | |
| path: build.log | |
| retention-days: 30 | |
| if-no-files-found: ignore | |
| # Run your build/test script INSIDE the built image. | |
| # Assumes your image contains your package source (COPY . ...) and that this script exists. | |
| - name: Run build/test script inside image | |
| run: | | |
| set -euo pipefail | |
| docker run --rm \ | |
| ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}:ci-${{ github.sha }} \ | |
| Rscript -f tests/build_test.R | |
| # Optional (but common): scan the image before release | |
| - name: Trivy scan (fail on HIGH/CRITICAL) | |
| uses: aquasecurity/trivy-action@0.28.0 | |
| with: | |
| image-ref: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}:ci-${{ github.sha }} | |
| format: table | |
| ignore-unfixed: true | |
| vuln-type: os,library | |
| severity: HIGH,CRITICAL | |
| exit-code: "1" | |
| # 2) Release build (PUSH) to GHCR and Azure CR with SBOM + provenance attestations. | |
| # Multi-platform Linux: amd64 (Intel/AMD) and arm64 (Apple Silicon, ARM). Windows containers need a separate Windows Dockerfile (different base OS). | |
| - name: Build & push (release + attestations) | |
| id: build_release | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: ./Dockerfile | |
| build-args: | | |
| BASE_IMAGE=executionengine.azurecr.io/${{ steps.image.outputs.name }}-base:latest | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: | | |
| ${{ steps.meta.outputs.tags }} | |
| executionengine.azurecr.io/${{ steps.image.outputs.name }}:sha-${{ github.sha }} | |
| executionengine.azurecr.io/${{ steps.image.outputs.name }}:${{ github.ref_name }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| # Supply-chain metadata | |
| provenance: true | |
| sbom: true | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| # Keyless signing using GitHub OIDC (no long-lived keys). | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@v3 | |
| with: | |
| cosign-release: v2.4.1 | |
| - name: Sign image digest (keyless) | |
| env: | |
| COSIGN_EXPERIMENTAL: "false" | |
| run: | | |
| set -euo pipefail | |
| IMAGE="${{ env.REGISTRY }}/${{ steps.image.outputs.name }}" | |
| DIGEST="${{ steps.build_release.outputs.digest }}" | |
| cosign sign --yes "${IMAGE}@${DIGEST}" | |
| # Optional: sign the SBOM/provenance attestations too (recommended if you plan to verify them client-side) | |
| - name: Sign attestations (keyless) | |
| run: | | |
| set -euo pipefail | |
| IMAGE="${{ env.REGISTRY }}/${{ steps.image.outputs.name }}" | |
| DIGEST="${{ steps.build_release.outputs.digest }}" | |
| # This signs the attached attestations (provenance/SBOM) for that digest. | |
| cosign sign-attestation --yes "${IMAGE}@${DIGEST}" | |