Skip to content

Commit b964e40

Browse files
committed
add smoke test + gate ci on it
1 parent db8c561 commit b964e40

4 files changed

Lines changed: 191 additions & 10 deletions

File tree

.github/workflows/docker-publish.yml

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,22 @@ env:
2121
REGISTRY: ghcr.io
2222
# github.repository as <account>/<repo>
2323
IMAGE_NAME: ${{ github.repository }}
24+
# arm/v7 dropped: ZM 1.38 has no armhf trixie packages
25+
IMAGE_PLATFORMS: linux/amd64,linux/arm64
2426

2527

2628
jobs:
29+
# gate: don't build or publish anything unless the tests pass
30+
test:
31+
runs-on: ubuntu-latest
32+
steps:
33+
- name: Checkout repository
34+
uses: actions/checkout@v5
35+
- name: Run tests
36+
run: ./test.sh
37+
2738
build:
39+
needs: test
2840

2941
runs-on: ubuntu-latest
3042
permissions:
@@ -36,20 +48,25 @@ jobs:
3648

3749
steps:
3850
- name: Checkout repository
39-
uses: actions/checkout@v4
51+
uses: actions/checkout@v5
4052

4153
- name: Install Cosign
4254
uses: sigstore/cosign-installer@v3.5.0
4355

56+
- name: Set up QEMU
57+
uses: docker/setup-qemu-action@v4
58+
with:
59+
platforms: ${{ env.IMAGE_PLATFORMS }}
60+
4461
# Workaround: https://github.com/docker/build-push-action/issues/461
4562
- name: Setup Docker buildx
46-
uses: docker/setup-buildx-action@v3
63+
uses: docker/setup-buildx-action@v4
4764

4865
# Login against a Docker registry except on PR
4966
# https://github.com/docker/login-action
5067
- name: Log into registry ${{ env.REGISTRY }}
5168
if: github.event_name != 'pull_request'
52-
uses: docker/login-action@v3
69+
uses: docker/login-action@v4
5370
with:
5471
registry: ${{ env.REGISTRY }}
5572
username: ${{ github.actor }}
@@ -59,12 +76,16 @@ jobs:
5976
# https://github.com/docker/metadata-action
6077
- name: Extract Docker metadata
6178
id: meta
62-
uses: docker/metadata-action@v5
79+
uses: docker/metadata-action@v6
6380
with:
6481
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
6582

6683
- name: Extract Docker Version Tag
67-
id: docker_version_tag
84+
id: docker_version_tag
85+
env:
86+
# rebuild on code changes (push / manual run); only the nightly
87+
# schedule dedups on the version tag to skip identical rebuilds
88+
FORCE_REBUILD: ${{ github.event_name != 'schedule' && '1' || '' }}
6889
run: |
6990
./get-version.sh $(echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:" | tr '[:upper:]' '[:lower:]') | sed 's/^/tag=/g' >> $GITHUB_OUTPUT
7091
@@ -77,12 +98,12 @@ jobs:
7798
- name: Build and push Docker image with tag
7899
if: ${{ !endsWith(steps.docker_version_tag.outputs.tag, 'latest') }}
79100
id: build-and-push-tagged
80-
uses: docker/build-push-action@v5
101+
uses: docker/build-push-action@v7
81102
with:
82103
context: .
83104
push: ${{ github.event_name != 'pull_request' }}
84105
tags: ${{ steps.docker_version_tag.outputs.tag }}
85-
platforms: linux/amd64,linux/arm64
106+
platforms: ${{ env.IMAGE_PLATFORMS }}
86107
labels: ${{ steps.meta.outputs.labels }}
87108
cache-from: type=gha
88109
cache-to: type=gha,mode=max
@@ -100,7 +121,7 @@ jobs:
100121

101122
- name: Build Docker Latest Tag
102123
if: ${{ !endsWith(steps.docker_version_tag.outputs.tag, 'latest') }}
103-
id: docker_latest_tag
124+
id: docker_latest_tag
104125
run: |
105126
echo "tag=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" | tr '[:upper:]' '[:lower:]' >> $GITHUB_OUTPUT
106127
@@ -109,12 +130,12 @@ jobs:
109130
- name: Build and push Docker image as latest
110131
if: ${{ !endsWith(steps.docker_version_tag.outputs.tag, 'latest') }}
111132
id: build-and-push-latest
112-
uses: docker/build-push-action@v5
133+
uses: docker/build-push-action@v7
113134
with:
114135
context: .
115136
push: ${{ github.event_name != 'pull_request' }}
116137
tags: ${{ steps.docker_latest_tag.outputs.tag }}
117-
platforms: linux/amd64,linux/arm64
138+
platforms: ${{ env.IMAGE_PLATFORMS }}
118139
labels: ${{ steps.meta.outputs.labels }}
119140
cache-from: type=gha
120141
cache-to: type=gha,mode=max

.github/workflows/test.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout repository
12+
uses: actions/checkout@v5
13+
14+
- name: Run smoke tests
15+
run: ./test.sh

get-version.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ export ZONEMINDER_VERSION=$(docker run --rm -t get-version dpkg --list zoneminde
77
[ -z "$DEBIAN_VERSION" ] && exit 1
88

99
export IMGTAG=$(echo "$1""d$DEBIAN_VERSION-a$APACHE_VERSION-zm$ZONEMINDER_VERSION")
10+
# FORCE_REBUILD (set by the workflow for push / manual runs) rebuilds even if
11+
# the versioned image already exists, so code/config changes get republished.
12+
# the nightly schedule leaves it unset and keeps deduping on the version tag.
13+
if [ -n "$FORCE_REBUILD" ]; then
14+
echo "$IMGTAG"
15+
exit 0
16+
fi
17+
1018
export IMAGE_EXISTS=$(docker pull "$IMGTAG" 2>/dev/null >/dev/null; echo $?)
1119

1220
# return latest, if container is already available :)

test.sh

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/usr/bin/env bash
2+
# Automated smoke test for the zoneminder container.
3+
# Builds the image, starts a mariadb sidecar on a shared docker network, runs
4+
# zoneminder pointed at it and asserts that apache + the ZoneMinder web console
5+
# come up and serve without PHP fatal errors.
6+
#
7+
# No host bind-mounts: the DB lives inside the sidecar container, and HTTP is
8+
# spoken from inside the zoneminder container with a bash /dev/tcp helper
9+
# (the apache2 base image ships no wget/curl).
10+
set -e
11+
12+
IMG=zoneminder-test
13+
CN=zoneminder-test-run
14+
DB=zoneminder-test-db
15+
NET=zoneminder-test-net
16+
17+
cleanup() {
18+
docker rm -f "$CN" >/dev/null 2>&1 || true
19+
docker rm -f "$DB" >/dev/null 2>&1 || true
20+
docker network rm "$NET" >/dev/null 2>&1 || true
21+
}
22+
trap cleanup EXIT
23+
24+
fail() { echo "FAIL: $1"; exit 1; }
25+
26+
# http_get <container> <path> -> full HTTP response (headers + body) via /dev/tcp
27+
http_get() {
28+
docker exec "$1" bash -c '
29+
exec 3<>/dev/tcp/127.0.0.1/80 || exit 1
30+
printf "GET '"$2"' HTTP/1.0\r\nHost: localhost\r\nConnection: close\r\n\r\n" >&3
31+
cat <&3
32+
'
33+
}
34+
35+
# http_console <container> -> fetch /zm/ and follow ZoneMinder's first-run
36+
# redirect (e.g. -> ?view=privacy) so we land on the real console HTML.
37+
http_console() {
38+
local resp loc
39+
resp=$(http_get "$1" /zm/)
40+
loc=$(echo "$resp" | grep -i '^Location:' | head -1 | sed 's/^[Ll]ocation:[[:space:]]*//; s/[[:space:]]*$//' | tr -d '\r')
41+
if [ -n "$loc" ]; then
42+
case "$loc" in
43+
/*) http_get "$1" "$loc" ;;
44+
*) http_get "$1" "/zm/$loc" ;;
45+
esac
46+
else
47+
echo "$resp"
48+
fi
49+
}
50+
51+
echo ">> building image"
52+
docker build -t "$IMG" .
53+
54+
echo ">> (re)creating docker network"
55+
docker network rm "$NET" >/dev/null 2>&1 || true
56+
docker network create "$NET" >/dev/null
57+
58+
echo ">> starting mariadb sidecar"
59+
docker rm -f "$DB" >/dev/null 2>&1 || true
60+
docker run -d --name "$DB" --network "$NET" \
61+
-e MYSQL_ROOT_PASSWORD=rootpass \
62+
-e MYSQL_DATABASE=zm \
63+
-e MYSQL_USER=zmuser \
64+
-e MYSQL_PASSWORD=zmpass \
65+
mariadb --max-allowed-packet=64MB >/dev/null
66+
67+
echo ">> waiting for mariadb to accept connections (up to 60s)"
68+
dbup=0
69+
for _ in $(seq 1 30); do
70+
if docker exec "$DB" mariadb-admin ping -uzmuser -pzmpass >/dev/null 2>&1; then dbup=1; break; fi
71+
sleep 2
72+
done
73+
[ "$dbup" = 1 ] || fail "mariadb sidecar did not become ready in time"
74+
echo "ok - mariadb ready"
75+
76+
echo ">> starting zoneminder"
77+
docker rm -f "$CN" >/dev/null 2>&1 || true
78+
docker run -d --name "$CN" --network "$NET" \
79+
--shm-size=2g \
80+
-e DISABLE_TLS=disable \
81+
-e ZM_DB_HOST="$DB" \
82+
-e ZM_DB_NAME=zm \
83+
-e ZM_DB_USER=zmuser \
84+
-e ZM_DB_PASS=zmpass \
85+
"$IMG" >/dev/null
86+
87+
echo ">> waiting for apache to listen on :80 (up to 90s)"
88+
up=0
89+
for _ in $(seq 1 45); do
90+
if docker exec "$CN" bash -c 'exec 3<>/dev/tcp/127.0.0.1/80' 2>/dev/null; then up=1; break; fi
91+
sleep 2
92+
done
93+
[ "$up" = 1 ] || fail "apache did not start listening on :80 in time"
94+
echo "ok - apache listening on :80"
95+
96+
echo ">> assert: container is running"
97+
[ "$(docker inspect -f '{{.State.Running}}' "$CN")" = true ] || fail "container not running"
98+
echo "ok - container running"
99+
100+
echo ">> waiting for the ZoneMinder console to serve (db init is slow, up to 120s)"
101+
zmup=0
102+
for _ in $(seq 1 60); do
103+
if http_console "$CN" 2>/dev/null | grep -q 'ZoneMinder'; then zmup=1; break; fi
104+
sleep 2
105+
done
106+
[ "$zmup" = 1 ] || fail "ZoneMinder console did not serve HTML in time"
107+
echo "ok - ZoneMinder console responding"
108+
109+
echo ">> assert: GET / redirects (302) to zm/"
110+
root=$(http_get "$CN" /)
111+
echo "$root" | head -1 | grep -q '302' || fail "GET / did not return 302 (got: $(echo "$root" | head -1))"
112+
echo "$root" | grep -qi '^Location:.*zm/' || fail "GET / 302 has no Location: zm/ header"
113+
echo "ok - GET / -> 302 Location: zm/"
114+
115+
echo ">> assert: the ZoneMinder web console serves HTML containing ZoneMinder"
116+
# GET /zm/ lands on the console; on a fresh install it first bounces through
117+
# ?view=privacy, so follow that one redirect before inspecting the body.
118+
zm=$(http_console "$CN")
119+
echo "$zm" | head -1 | grep -q '200' || fail "console did not return 200 (got: $(echo "$zm" | head -1))"
120+
echo "$zm" | grep -q 'ZoneMinder' || fail "console body does not contain 'ZoneMinder'"
121+
echo "ok - console serves ZoneMinder HTML ($(echo "$zm" | grep -io '<title>[^<]*</title>' | head -1))"
122+
123+
# The very first requests during db bootstrap can log a transient logger.php
124+
# fatal (config constants not defined yet); that window is over once the
125+
# console serves above. Truncate the log, hit the console fresh and assert the
126+
# steady-state console renders without any PHP fatal.
127+
echo ">> assert: no PHP Fatal errors serving the console (steady state)"
128+
docker exec "$CN" sh -c ': > /var/log/apache2/error.log' 2>/dev/null || true
129+
http_console "$CN" >/dev/null 2>&1
130+
http_get "$CN" '/zm/?view=console' >/dev/null 2>&1
131+
sleep 2
132+
logs=$( { docker exec "$CN" cat /var/log/apache2/error.log 2>/dev/null; docker logs "$CN" 2>&1; } || true )
133+
echo "$logs" | grep -i 'PHP Fatal' && fail "PHP Fatal error found while serving the console" || true
134+
echo "ok - no PHP Fatal errors"
135+
136+
echo ""
137+
echo "ALL TESTS PASSED"

0 commit comments

Comments
 (0)