Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/test-check-if-latest-lts-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ jobs:
with:
hz_version: 5.5.0

- name: Run check-if-latest-lts-release 5.5.8
id: check-if-latest-lts-release-5_5_8
- name: Run check-if-latest-lts-release 5.5.9
id: check-if-latest-lts-release-5_5_9
uses: ./docker-actions/check-if-latest-lts-release
with:
hz_version: 5.5.8
hz_version: 5.5.9

- name: Run check-if-latest-lts-release 5.6.0
id: check-if-latest-lts-release-5_6_0
Expand All @@ -51,7 +51,7 @@ jobs:
}

assert_equals "false" "${{ steps.check-if-latest-lts-release-5_5_0.outputs.is_latest_lts }}"
assert_equals "true" "${{ steps.check-if-latest-lts-release-5_5_8.outputs.is_latest_lts }}"
assert_equals "true" "${{ steps.check-if-latest-lts-release-5_5_9.outputs.is_latest_lts }}"
assert_equals "false" "${{ steps.check-if-latest-lts-release-5_6_0.outputs.is_latest_lts }}"

assert_eq 0 "$TESTS_RESULT" "All tests should pass"
20 changes: 20 additions & 0 deletions .github/workflows/test-verify-docker-reproducibility.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Test verify-docker-reproducibility action

on:
workflow_call:

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Test scripts
run: ./verify-docker-reproducibility/test_scripts.sh

- name: Run verify-docker-reproducibility
uses: ./verify-docker-reproducibility
with:
dockerfile: verify-docker-reproducibility/test-data/reproducible.Dockerfile
extra-args: verify-docker-reproducibility/test-data
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ jobs:
uses: ./.github/workflows/test-slack-notification.yml
secrets: inherit

test-verify-docker-reproducibility:
uses: ./.github/workflows/test-verify-docker-reproducibility.yml

assert-all-jobs-succeeded:
runs-on: ubuntu-latest
needs:
Expand All @@ -74,6 +77,7 @@ jobs:
- test-resolve-editions
- test-setup-maven-snapshot-internal
- test-slack-notification
- test-verify-docker-reproducibility
if: always()
steps:
- name: Check all jobs succeeded
Expand Down
23 changes: 23 additions & 0 deletions verify-docker-reproducibility/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Verify Docker reproducibility
description: Verifies Docker image build reproducibility by building twice and comparing layer digests

inputs:
dockerfile:
description: Path to the Dockerfile
required: true
extra-args:
description: Extra arguments passed to docker buildx build (e.g. context path, --output flags)
required: false
default: ""

runs:
using: "composite"
steps:
- shell: bash
run: |
${GITHUB_ACTION_PATH}/verify-docker-reproducibility.sh \
"${DOCKERFILE}" \
${EXTRA_ARGS}
env:
DOCKERFILE: ${{ inputs.dockerfile }}
EXTRA_ARGS: ${{ inputs.extra-args }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM alpine:3:20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
FROM alpine:3:20
FROM alpine:latest

Just in case there's some CVE or something that leave us vulnerable to supply chain attack.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only a test image, and sonar complains about it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. To be fair renovate could manage this for us…

RUN echo "built-at: $(date +%s%N)" > /build-timestamp \
&& sleep 1
USER nobody
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FROM alpine:3.20
USER nobody
5 changes: 5 additions & 0 deletions verify-docker-reproducibility/test_scripts.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"

find "$SCRIPT_DIR" -name "*_tests.sh" -print0 | xargs -0 -n1 bash
115 changes: 115 additions & 0 deletions verify-docker-reproducibility/verify-docker-reproducibility.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env bash

set -o errexit -o nounset -o pipefail ${RUNNER_DEBUG:+-x}

# shellcheck source=../.github/scripts/logging.functions.sh
source .github/scripts/logging.functions.sh

readonly RANDOM_SUFFIX="$(head -c 8 /dev/urandom | od -An -tx1 | tr -d ' \n')"
readonly TAG_A="repro-check-a-${RANDOM_SUFFIX}"
readonly TAG_B="repro-check-b-${RANDOM_SUFFIX}"

cleanup() {
docker rmi "${TAG_A}" "${TAG_B}" 2>/dev/null
return 0
}
trap cleanup EXIT

if [[ "$#" -lt 1 ]]; then
echo "Verifies Docker image build reproducibility by building twice and comparing layer digests."
echo "Exits 0 if all layers are identical across both builds, 1 if any differ."
echo ""
echo "Usage: $0 <dockerfile> [extra buildx build args...]"
echo ""
echo "Examples:"
echo " $0 hazelcast-oss/Dockerfile hazelcast-oss/"
echo " $0 hazelcast-oss/Dockerfile --output type=docker,rewrite-timestamp=true hazelcast-oss/"
exit 1
fi
readonly dockerfile="${1:?Error: <dockerfile> is required}"
shift

# Add --load if user didn't provide --output
load_flag="--load"
for arg in "$@"; do
case "${arg}" in
--output|--output=*) load_flag="" ;;
*) ;;
esac
done

build_image() {
local tag="$1"
shift
echo "==> Building image '${tag}'..."
docker buildx build \
--no-cache \
${load_flag} \
-f "${dockerfile}" \
-t "${tag}" \
"$@"
return $?
}

get_layers() {
local tag="$1"
docker inspect --format '{{json .RootFS.Layers}}' "${tag}"
return $?
}

get_digest() {
local layers="$1"
local index="$2"
echo "${layers}" | jq --raw-output ".[${index}] // empty"
return $?
}

build_image "${TAG_A}" "$@"
echo ""
build_image "${TAG_B}" "$@"
echo ""

layers_a=$(get_layers "${TAG_A}")
layers_b=$(get_layers "${TAG_B}")

layer_count_a=$(echo "${layers_a}" | jq 'length')
layer_count_b=$(echo "${layers_b}" | jq 'length')

echo "=== Layer Reproducibility Report ==="
echo ""

if [[ "${layer_count_a}" -ne "${layer_count_b}" ]]; then
echowarning "WARNING: Layer count mismatch (${layer_count_a} vs ${layer_count_b})"
echo ""
fi

layer_max_count=$(( layer_count_a > layer_count_b ? layer_count_a : layer_count_b ))
has_diff=false
for layerIndex in $(seq 0 $((layer_max_count - 1))); do
layer_digest_a=$(get_digest "${layers_a}" "${layerIndex}")
layer_digest_b=$(get_digest "${layers_b}" "${layerIndex}")
if [[ -z "${layer_digest_a}" ]]; then
echo " Layer $((layerIndex + 1))/${layer_max_count}: ONLY IN B"
echo " B: ${layer_digest_b}"
has_diff=true
elif [[ -z "${layer_digest_b}" ]]; then
echo " Layer $((layerIndex + 1))/${layer_max_count}: ONLY IN A"
echo " A: ${layer_digest_a}"
has_diff=true
elif [[ "${layer_digest_a}" == "${layer_digest_b}" ]]; then
echo " Layer $((layerIndex + 1))/${layer_max_count}: MATCH"
else
echo " Layer $((layerIndex + 1))/${layer_max_count}: DIFFER"
echo " A: ${layer_digest_a}"
echo " B: ${layer_digest_b}"
has_diff=true
fi
done

echo ""
if [[ "${has_diff}" == "true" ]]; then
echoerr "RESULT: FAIL - some layers differ between builds"
exit 1
fi

echo "RESULT: PASS - all ${layer_count_a} layers are identical"
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash

set -eu ${RUNNER_DEBUG:+-x}

SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"

# Source the latest version of assert.sh unit testing library and include in current shell
source /dev/stdin <<< "$(curl --silent https://raw.githubusercontent.com/hazelcast/assert.sh/main/assert.sh)"

TESTS_RESULT=0

log_header "Tests for verify-docker-reproducibility"

log_header "Should exit 1 when no arguments provided"
"$SCRIPT_DIR"/verify-docker-reproducibility.sh && true
actual_exit_code=$?
assert_eq 1 "$actual_exit_code" "Should exit 1 when no args given" && log_success "Exits 1 with no args" || TESTS_RESULT=$?

log_header "Should pass for a reproducible image"
"$SCRIPT_DIR"/verify-docker-reproducibility.sh "$SCRIPT_DIR/test-data/reproducible.Dockerfile" "$SCRIPT_DIR/test-data" && true
actual_exit_code=$?
assert_eq 0 "$actual_exit_code" "Should exit 0 for reproducible build" && log_success "Reproducible build passes" || TESTS_RESULT=$?

log_header "Should fail for a non-reproducible image"
"$SCRIPT_DIR"/verify-docker-reproducibility.sh "$SCRIPT_DIR/test-data/non-reproducible.Dockerfile" "$SCRIPT_DIR/test-data" && true
actual_exit_code=$?
assert_eq 1 "$actual_exit_code" "Should exit 1 for non-reproducible build" && log_success "Non-reproducible build fails" || TESTS_RESULT=$?

assert_eq 0 "$TESTS_RESULT" "All tests should pass"
Loading