diff --git a/.ci/check_image_size b/.ci/check_image_size new file mode 100755 index 000000000..2935d13c4 --- /dev/null +++ b/.ci/check_image_size @@ -0,0 +1,91 @@ +#!/bin/sh +# check_image_size IMAGE_REFERENCE LIMIT_GIB +# +# Inspects a single-arch OCI/Docker image manifest (or the first image +# manifest in a multi-arch OCI/Docker image) that has already been pushed +# to a registry and fails with a clear message if the total compressed layer +# size exceeds LIMIT_GIB gibibytes. +# +# Requires: docker (with buildx), jq, bc +# Usage: +# .ci/check_image_size ghcr.io/eic/eic_xl@sha256:abc... 10 +# .ci/check_image_size ghcr.io/eic/eic_xl:master 10 +# +# The caller is responsible for being logged in to the registry before calling +# this script. + +set -e + +IMAGE_REF="${1:?Usage: $0 IMAGE_REFERENCE LIMIT_GIB}" +LIMIT_GIB="${2:?Usage: $0 IMAGE_REFERENCE LIMIT_GIB}" + +echo "Checking compressed image size for ${IMAGE_REF} (limit: ${LIMIT_GIB} GiB) ..." + +MANIFEST=$(docker buildx imagetools inspect --raw "${IMAGE_REF}") + +# docker/build-push-action (v4+) wraps even single-platform pushes in an OCI +# Image Index when provenance attestations are enabled (the default). Detect +# that case and resolve to the real per-platform Image Manifest before summing +# layer sizes. +MEDIA_TYPE=$(printf '%s' "${MANIFEST}" | jq -r '.mediaType // ""') +case "${MEDIA_TYPE}" in + *index* | *manifest.list*) + DIGEST=$(printf '%s' "${MANIFEST}" | jq -r ' + .manifests[] + | select( + .artifactType == null + and ((.annotations["vnd.docker.reference.type"] // "") != "attestation-manifest") + and ((.annotations["vnd.docker.reference.digest"] // "") == "") + and .platform != null + and (.platform.os // "") != "" + and (.platform.architecture // "") != "" + and (.platform.os // "") != "unknown" + and (.platform.architecture // "") != "unknown" + ) + | .digest' | head -1) + if [ -z "${DIGEST}" ]; then + echo "ERROR: OCI Image Index contains no platform manifest." + exit 1 + fi + IMAGE_BASE="${IMAGE_REF%%@*}" + # Strip :tag from the last path segment only (preserves registry host:port) + _dir="${IMAGE_BASE%/*}" + _name="${IMAGE_BASE##*/}" + if [ "${_dir}" != "${IMAGE_BASE}" ]; then + IMAGE_BASE="${_dir}/${_name%%:*}" + else + IMAGE_BASE="${_name%%:*}" + fi + MANIFEST=$(docker buildx imagetools inspect --raw "${IMAGE_BASE}@${DIGEST}") + ;; +esac + +SIZE_BYTES=$(printf '%s' "${MANIFEST}" | jq '[.layers[].size] | add // 0') + +if [ -z "${SIZE_BYTES}" ] || [ "${SIZE_BYTES}" = "null" ] || [ "${SIZE_BYTES}" -eq 0 ]; then + echo "ERROR: Could not determine image size from manifest (got: ${SIZE_BYTES})." + echo " Manifest snippet: $(printf '%s' "${MANIFEST}" | head -c 500)" + exit 1 +fi + +SIZE_CENTI_GIB=$(echo "(${SIZE_BYTES} * 100 + 1073741824 - 1) / 1073741824" | bc) +SIZE_GIB="$(echo "${SIZE_CENTI_GIB} / 100" | bc).$(printf '%02d' "$(echo "${SIZE_CENTI_GIB} % 100" | bc)")" +LIMIT_BYTES=$(echo "${LIMIT_GIB} * 1073741824 / 1" | bc) + +echo " Compressed size : ${SIZE_GIB} GiB (${SIZE_BYTES} bytes)" +echo " Limit : ${LIMIT_GIB} GiB (${LIMIT_BYTES} bytes)" + +if [ "${SIZE_BYTES}" -gt "${LIMIT_BYTES}" ]; then + echo "" + echo "ERROR: Image ${IMAGE_REF} exceeds the size cap!" + echo " ${SIZE_GIB} GiB > ${LIMIT_GIB} GiB" + echo "" + echo "To update the high-water mark after an intentional size increase:" + echo " 1. Re-run this script to inspect the published image size:" + echo " .ci/check_image_size \"${IMAGE_REF}\" \"${LIMIT_GIB}\"" + echo " 2. Add ~15% buffer, and optionally round up to the next whole GiB" + echo " 3. Update SIZE_LIMIT_*_GIB in .github/workflows/build-push.yml and .gitlab-ci.yml" + exit 1 +fi + +echo " OK: size is within the ${LIMIT_GIB} GiB limit." diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index 95a51404d..7f0492de5 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -56,6 +56,15 @@ env: ## Internal tag used for the CI INTERNAL_TAG: pipeline-${{ github.run_id }} + ## Compressed image size caps (GiB). Fail the build if a final image exceeds + ## these limits to catch regressions early (e.g. accidental Spack-built LLVM). + ## To update: measure the compressed sizes of the published master images, + ## add ~15% headroom, round up, and update here and in .gitlab-ci.yml + ## variables. + SIZE_LIMIT_DEBIAN_STABLE_BASE_GIB: 0.8 + SIZE_LIMIT_EIC_CI_GIB: 3 + SIZE_LIMIT_EIC_XL_GIB: 4.5 + jobs: env: ## The env context is not available in matrix, only in steps, @@ -249,6 +258,18 @@ jobs: run: | mkdir -p /tmp/digests echo "${{ steps.meta.outputs.tags }}@${{ steps.build.outputs.digest }}" > /tmp/digests/${{ matrix.BUILD_IMAGE }}-${{ matrix.arch }}.digest + - name: Check image size + run: | + FULLNAME="${{ matrix.BUILD_IMAGE }}" + LIMIT_VAR="SIZE_LIMIT_$(printf '%s' "${FULLNAME}" | tr '[:lower:]' '[:upper:]')_GIB" + LIMIT="${!LIMIT_VAR}" + if [ -n "${LIMIT}" ]; then + .ci/check_image_size \ + "${{ env.GH_REGISTRY }}/${{ env.GH_REGISTRY_USER }}/${FULLNAME}@${{ steps.build.outputs.digest }}" \ + "${LIMIT}" + else + echo "No size limit configured for ${FULLNAME}, skipping check." + fi - name: Upload digest as artifact uses: actions/upload-artifact@v7 with: @@ -570,6 +591,20 @@ jobs: run: | mkdir -p /tmp/digests echo "${{ steps.meta.outputs.tags }}@${{ steps.build.outputs.digest }}" > /tmp/digests/${{ matrix.arch }}.digest + - name: Check image size + # Only check final images; concretization-only targets (cuda, tf) don't produce a full image + if: ${{ matrix.target == 'final' }} + run: | + FULLNAME="${{ matrix.BUILD_IMAGE }}${{ matrix.ENV }}" + LIMIT_VAR="SIZE_LIMIT_$(printf '%s' "${FULLNAME}" | tr '[:lower:]' '[:upper:]')_GIB" + LIMIT="${!LIMIT_VAR}" + if [ -n "${LIMIT}" ]; then + .ci/check_image_size \ + "${{ env.GH_REGISTRY }}/${{ env.GH_REGISTRY_USER }}/${FULLNAME}@${{ steps.build.outputs.digest }}" \ + "${LIMIT}" + else + echo "No size limit configured for ${FULLNAME}, skipping check." + fi - name: Upload digest as artifact uses: actions/upload-artifact@v7 with: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d7d701bd4..90560433e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -29,6 +29,15 @@ variables: ## Number of jobs to start during container builds JOBS: 32 + ## Compressed image size caps (GiB). Fail the build if a final image exceeds + ## these limits to catch regressions early (e.g. accidental Spack-built LLVM). + ## To update: measure the current master-tag compressed image sizes using + ## registry or CI tooling, add ~15% headroom, round up, and update here and + ## in .github/workflows/build-push.yml env. + SIZE_LIMIT_DEBIAN_STABLE_BASE_GIB: "0.8" + SIZE_LIMIT_EIC_CI_GIB: "3" + SIZE_LIMIT_EIC_XL_GIB: "4.5" + ## is this nightly or not? NIGHTLY: "" ## Add to tag @@ -238,6 +247,7 @@ status:pending: - docker-new before_script: - !reference [.docker, before_script] + - apk add bc git jq - if [ "$SKIP_BINFMT" != "true" ]; then mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc ; for arch in aarch64 ; do @@ -355,6 +365,15 @@ base: --provenance false containers/debian 2>&1 | tee build.log + - LIMIT_VAR="SIZE_LIMIT_$(printf '%s' "${BUILD_IMAGE}" | tr '[:lower:]' '[:upper:]')_GIB" ; + LIMIT=$(printenv "${LIMIT_VAR}" || true) ; + if [ -n "${LIMIT}" ]; then + .ci/check_image_size + "${CI_REGISTRY}/${CI_PROJECT_PATH}/${BUILD_IMAGE}:${INTERNAL_TAG}" + "${LIMIT}" ; + else + echo "No size limit configured for ${BUILD_IMAGE}, skipping check." ; + fi eic: parallel: @@ -503,6 +522,16 @@ eic: --provenance false containers/eic 2>&1 | tee build.log + - FULLNAME="${BUILD_IMAGE}${ENV}" ; + LIMIT_VAR="SIZE_LIMIT_$(printf '%s' "${FULLNAME}" | tr '[:lower:]' '[:upper:]')_GIB" ; + LIMIT=$(printenv "${LIMIT_VAR}" || true) ; + if [ -n "${LIMIT}" ]; then + .ci/check_image_size + "${CI_REGISTRY}/${CI_PROJECT_PATH}/${FULLNAME}:${INTERNAL_TAG}-${BUILD_TYPE}" + "${LIMIT}" ; + else + echo "No size limit configured for ${FULLNAME}, skipping check." ; + fi user_spack_environment: stage: benchmarks diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dca79b662..fb2d78cb2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,4 +32,4 @@ repos: - id: shellcheck types: [shell] args: [--severity=warning, -x] - files: (^[^/]+\.sh$|^\.ci/resolve_git_ref$) + files: (^[^/]+\.sh$|^\.ci/resolve_git_ref$|^\.ci/check_image_size$)