diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 72e25b2..cc5e339 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,6 +32,9 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Test entrypoint script + run: sh docker-entrypoint-test.sh + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 105b98b..5eebcab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - `BACKUP_INTERVAL_MS` environment variable for the Docker daemon image, mapped by `docker-entrypoint.sh` to `-Dbackup.scheduled.interval.ms`, so the backup interval can be configured from `.env`/`docker-compose.yml` without overriding the entrypoint +- `docker-entrypoint-test.sh`, a shell test that runs `docker-entrypoint.sh` against a stub `java` and asserts the argument list built for each combination of `BACKUP_DIRECTORY`, `SCHEDULED_USERS` and `BACKUP_INTERVAL_MS`, including empty values and values containing spaces; it runs in the `docker-build` CI job, so entrypoint changes are no longer merged unexecuted ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7e58a2a..c4c4e06 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,6 +69,14 @@ Windows: mvn clean test ``` +The Docker entrypoint script is covered by a separate shell test, which runs `docker-entrypoint.sh` against a stub `java` and asserts the arguments it builds. It requires a POSIX shell, so run it on Linux / macOS (or under WSL or Git Bash on Windows): + +```bash +sh docker-entrypoint-test.sh +``` + +Both suites run in CI on every pull request. + ## Questions Ask in the [Discord server](https://discord.gg/xXtuAQ2). diff --git a/README.md b/README.md index a041406..16d962c 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,24 @@ mvn clean test If you see `BUILD SUCCESS`, the tests have passed. +### Entrypoint Script Test + +`docker-entrypoint.sh` is covered by a shell test that runs it against a stub `java` and asserts the arguments it builds. A POSIX shell is required, so on Windows it is run under WSL or Git Bash. + +Linux / macOS: + +```bash +sh docker-entrypoint-test.sh +``` + +Windows (Git Bash / WSL): + +```bash +sh docker-entrypoint-test.sh +``` + +If you see `All docker-entrypoint.sh tests passed.`, the tests have passed. + ## Development ### Building and Running Locally diff --git a/docker-entrypoint-test.sh b/docker-entrypoint-test.sh new file mode 100755 index 0000000..3e43b85 --- /dev/null +++ b/docker-entrypoint-test.sh @@ -0,0 +1,118 @@ +#!/bin/sh +# Tests docker-entrypoint.sh by putting a stub `java` on PATH that prints its +# arguments instead of starting a JVM, then asserting the argument list the +# entrypoint builds for each combination of the environment variables it reads. +# +# Each argument is printed wrapped in brackets, so a value containing a space is +# distinguishable from two separate arguments. +# +# Run from the repository root: +# sh docker-entrypoint-test.sh + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +ENTRYPOINT="$SCRIPT_DIR/docker-entrypoint.sh" + +if [ ! -f "$ENTRYPOINT" ]; then + echo "docker-entrypoint.sh not found at $ENTRYPOINT" >&2 + exit 1 +fi + +STUB_BIN=$(mktemp -d) +trap 'rm -rf "$STUB_BIN"' EXIT INT TERM + +cat > "$STUB_BIN/java" <<'STUB' +#!/bin/sh +for arg in "$@"; do + printf '[%s]' "$arg" +done +echo +STUB +chmod +x "$STUB_BIN/java" + +PROFILE="[-Dspring.profiles.active=daemon]" +JAR="[-jar][gh-backup.jar]" + +failures=0 + +# Runs the entrypoint with only the given NAME=VALUE assignments exported, so a +# variable set in the caller's environment cannot leak into a case. Callers +# invoke this inside a command substitution, so the changes stay in a subshell. +run_entrypoint() { + unset BACKUP_DIRECTORY SCHEDULED_USERS BACKUP_INTERVAL_MS + for assignment in "$@"; do + case $assignment in + BACKUP_DIRECTORY=*) + BACKUP_DIRECTORY=${assignment#*=} + export BACKUP_DIRECTORY + ;; + SCHEDULED_USERS=*) + SCHEDULED_USERS=${assignment#*=} + export SCHEDULED_USERS + ;; + BACKUP_INTERVAL_MS=*) + BACKUP_INTERVAL_MS=${assignment#*=} + export BACKUP_INTERVAL_MS + ;; + *) + echo "Not a variable docker-entrypoint.sh reads: $assignment" >&2 + exit 1 + ;; + esac + done + PATH="$STUB_BIN:$PATH" sh "$ENTRYPOINT" +} + +expect() { + description=$1 + expected=$2 + shift 2 + + actual=$(run_entrypoint "$@") + + if [ "$actual" = "$expected" ]; then + echo "PASS: $description" + else + echo "FAIL: $description" + echo " expected: $expected" + echo " actual: $actual" + failures=$((failures + 1)) + fi +} + +expect "no variables set" \ + "$PROFILE$JAR" + +expect "BACKUP_DIRECTORY only" \ + "$PROFILE[-Dbackup.directory=/data/backups]$JAR" \ + "BACKUP_DIRECTORY=/data/backups" + +expect "SCHEDULED_USERS only" \ + "$PROFILE[-Dbackup.scheduled.users=octocat,github]$JAR" \ + "SCHEDULED_USERS=octocat,github" + +expect "BACKUP_INTERVAL_MS only" \ + "$PROFILE[-Dbackup.scheduled.interval.ms=3600000]$JAR" \ + "BACKUP_INTERVAL_MS=3600000" + +expect "all variables set, in declaration order" \ + "$PROFILE[-Dbackup.directory=/backups][-Dbackup.scheduled.users=octocat][-Dbackup.scheduled.interval.ms=60000]$JAR" \ + "BACKUP_DIRECTORY=/backups" "SCHEDULED_USERS=octocat" "BACKUP_INTERVAL_MS=60000" + +# The Dockerfile ships SCHEDULED_USERS and BACKUP_INTERVAL_MS as empty defaults, +# so an empty value must be treated as unset rather than passed through as an +# empty system property. +expect "empty variables add no arguments" \ + "$PROFILE$JAR" \ + "BACKUP_DIRECTORY=" "SCHEDULED_USERS=" "BACKUP_INTERVAL_MS=" + +expect "value containing a space stays a single argument" \ + "$PROFILE[-Dbackup.directory=/data/my backups]$JAR" \ + "BACKUP_DIRECTORY=/data/my backups" + +if [ "$failures" -eq 0 ]; then + echo "All docker-entrypoint.sh tests passed." + exit 0 +fi + +echo "$failures docker-entrypoint.sh test(s) failed." >&2 +exit 1