Skip to content
Closed
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
2 changes: 2 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:

steps:
- uses: actions/checkout@v4
- name: Test image publishing command
run: ./ci/tests/publish-image-test.sh
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker Image
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ test_env*
.pids
/test-env-compose/grafana/*
/test-env-compose/prometheus/*
/image-size-report.md
55 changes: 55 additions & 0 deletions Jenkinsfile.image
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
pipeline {
agent any

options {
disableConcurrentBuilds()
timestamps()
}

parameters{
string(
name: 'RELEASE_REF',
defaultValue: '',
description: 'Release ref such as refs/tags/1.2.3; defaults to GIT_BRANCH'
)
}

environment {
IMAGE_REPOSITORY = 'registry.onlinedi.vision:5000/od-official-server'
IMAGE_REPORT = 'image-size-report.md'
}

stages {
stage('Test publishing command') {
steps {
sh './ci/tests/publish-image-test.sh'
}
}

stage('Build and push image') {
steps{
withDockerRegistry(
url: 'https://registry.onlinedi.vision:5000',
credentialsId: 'docker-registry'
){
sh './ci/publish-image.sh'
}
}
}
stage('Show image report') {
steps {
sh 'sed -n \'1,120p\' "$IMAGE_REPORT"'
}
}
}

post {
always {
archiveArtifacts(
artifacts: 'image-size-report.md',
allowEmptyArchive: true
)
}
}

}
102 changes: 102 additions & 0 deletions ci/publish-image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env bash

# Build, push, and report metadata for one tagged release image.

set -euo pipefail

# Prefer Jenkins' manual parameter, falling back to the checked-out Git ref.

git_ref="${RELEASE_REF:-${GIT_BRANCH:-}}"
image_repository="${IMAGE_REPOSITORY:-registry.onlinedi.vision:5000/od-official-server}"
report_file="${IMAGE_REPORT:-image-size-report.md}"

# Prevent branches and malformed refs from publishing release images.

if [[ ! "$git_ref" =~ ^refs/tags/([^/]+)$ ]]; then
printf 'Ref must have the form refs/tags/VERSION; received: %s\n' \
"${git_ref:-<empty>}" >&2
exit 2
fi

release_version="${BASH_REMATCH[1]}"
image_tag="v${release_version}"

# Ensure the generated value is a valid Docker image tag.

if [[ ! "$image_tag" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]]; then
printf 'Invalid container tag: %s\n' "$image_tag" >&2
exit 2
fi

image_ref="${image_repository}:${image_tag}"

# docker-bake.hcl expects this variable.
export GIT_BRANCH="$git_ref"

# Build and load one image into Docker.
docker buildx bake release \
--set 'release.output=type=docker' \
--set "release.tags=${image_ref}"

# Push the exact image that was just built.
docker push "$image_ref"

size_bytes="$(docker image inspect --format '{{.Size}}' "$image_ref")"

# Read local image metadata after the image has been pushed.

if [[ ! "$size_bytes" =~ ^[0-9]+$ ]]; then
printf 'Docker returned an invalid image size: %s\n' "$size_bytes" >&2
exit 1
fi

size_human="$(awk -v bytes="$size_bytes" 'BEGIN {
split("B KiB MiB GiB TiB", units, " ");
size = bytes;
unit = 1;

while (size >= 1024 && unit < 5) {
size /= 1024;
unit++;
}

if (unit == 1)
printf "%d %s", size, units[unit];
else
printf "%.2f %s", size, units[unit];
}')"

image_id="$(docker image inspect --format '{{.Id}}' "$image_ref")"
repo_digests="$(
docker image inspect \
--format '{{range .RepoDigests}}{{println .}}{{end}}' \
"$image_ref"
)"

repo_digest="unavailable"

while IFS= read -r candidate; do
if [[ -n "$candidate" ]]; then
repo_digest="$candidate"
break
fi
done <<< "$repo_digests"

git_commit="${GIT_COMMIT:-unknown}"

{
printf '# Container image report\n\n'
printf '| Field | Value |\n'
printf '| --- | --- |\n'
printf '| Image | `%s` |\n' "$image_ref"
printf '| Repository digest | `%s` |\n' "$repo_digest"
printf '| Image ID | `%s` |\n' "$image_id"
printf '| Local image size | %s (%s bytes) |\n' \
"$size_human" "$size_bytes"
printf '| Git commit | `%s` |\n' "$git_commit"
printf '| Generated at | %s |\n' \
"$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
} > "$report_file"

printf 'Published %s\n' "$image_ref"
printf 'Image size: %s (%s bytes)\n' "$size_human" "$size_bytes"
53 changes: 53 additions & 0 deletions ci/tests/fake-docker
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env bash

# Minimal Docker replacement used by tests; it never contacts a real daemon.

set -euo pipefail

# Record every simulated Docker command so the test can count and inspect them.

printf '%s\n' "$*" >> "${FAKE_DOCKER_LOG:?FAKE_DOCKER_LOG must be set}"

# Simulate either a successful build or a requested build failure.

if [[ "$1" == "buildx" && "${2:-}" == "bake" ]]; then
if [[ "${FAKE_DOCKER_FAIL_BUILD:-0}" == "1" ]]; then
exit 41
fi

exit 0
fi

# Accept the push command without contacting the registry.

if [[ "$1" == "push" ]]; then
exit 0
fi

# Return deterministic metadata for report-generation tests.

if [[ "$1" == "image" && "${2:-}" == "inspect" ]]; then
case "${4:-}" in
'{{.Size}}')
printf '12582912\n'
;;
'{{.Id}}')
printf 'sha256:image-id\n'
;;
'{{range .RepoDigests}}{{println .}}{{end}}')
printf '%s\n' \
'registry.onlinedi.vision:5000/od-official-server@sha256:digest'
;;
*)
printf 'Unexpected inspect format: %s\n' "${4:-}" >&2
exit 3
;;
esac

exit 0
fi

# Fail when the production script starts using an unhandled Docker command.

printf 'Unexpected Docker command: %s\n' "$*" >&2
exit 3
90 changes: 90 additions & 0 deletions ci/tests/publish-image-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash

# Verify publishing behavior safely by replacing Docker with the local fake.

set -euo pipefail

# Keep fake commands, logs, and reports isolated from the repository.

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
test_directory="$(mktemp -d)"
trap 'rm -rf "$test_directory"' EXIT

fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}

# Place the fake first in PATH under the name expected by the production script.

cp "$repo_root/ci/tests/fake-docker" "$test_directory/docker"
chmod +x "$test_directory/docker"

docker_log="$test_directory/docker.log"
report_file="$test_directory/image-size-report.md"

# Happy path: a valid release must build once, push once, and create a report.

FAKE_DOCKER_LOG="$docker_log" \
PATH="$test_directory:$PATH" \
RELEASE_REF='refs/tags/1.2.3' \
GIT_COMMIT='0123456789abcdef' \
IMAGE_REPORT="$report_file" \
"$repo_root/ci/publish-image.sh"

# Confirm that publishing did not rebuild or push the image more than once.

build_count="$(grep -c '^buildx bake release ' "$docker_log")"
push_count="$(
grep -c \
'^push registry.onlinedi.vision:5000/od-official-server:v1.2.3$' \
"$docker_log"
)"

[[ "$build_count" == "1" ]] ||
fail 'expected exactly one image build'

[[ "$push_count" == "1" ]] ||
fail 'expected exactly one image push'

# Confirm that the archived report contains the expected size and digest.

grep -q '12.00 MiB (12582912 bytes)' "$report_file" ||
fail 'report is missing the image size'

grep -q 'sha256:digest' "$report_file" ||
fail 'report is missing the pushed digest'


# Safety case: branch refs must be rejected before Docker is called.

: > "$docker_log"

if FAKE_DOCKER_LOG="$docker_log" \
PATH="$test_directory:$PATH" \
RELEASE_REF='refs/heads/main' \
"$repo_root/ci/publish-image.sh"; then
fail 'branch references must not be publishable'
fi

[[ ! -s "$docker_log" ]] ||
fail 'invalid refs must be rejected before calling Docker'

# Failure case: a failed build must stop execution before the push.

: > "$docker_log"

if FAKE_DOCKER_LOG="$docker_log" \
FAKE_DOCKER_FAIL_BUILD=1 \
PATH="$test_directory:$PATH" \
RELEASE_REF='refs/tags/2.0.0' \
"$repo_root/ci/publish-image.sh"; then
fail 'a failed build must fail the publishing command'
fi

failed_pushes="$(grep -c '^push ' "$docker_log" || true)"

[[ "$failed_pushes" == "0" ]] ||
fail 'the script pushed an image after a failed build'

printf 'PASS: image publishing and report generation\n'
Loading