HH-500: add reproducible production dist bundle (#111)

* HH-500: add reproducible production dist bundle

* HH-500: make production bundle builds reproducible

* HH-500: lock complete runtime APK closure

---------

Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
Rogee
2026-08-23 00:22:20 +08:00
committed by GitHub
co-authored by rogee
parent ef8931099c
commit a5d244d293
10 changed files with 279 additions and 2 deletions
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cd "$root"
if [[ -n $(git status --porcelain --untracked-files=normal) ]]; then
echo "refusing production build from a dirty worktree" >&2
exit 1
fi
version=${GOCHAT_VERSION:-$(node -p "require('./package.json').version")}
[[ $version =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || {
echo "invalid GOCHAT_VERSION: $version" >&2
exit 1
}
commit=$(git rev-parse --short=12 HEAD)
build_date=$(git show -s --format=%cI HEAD)
source_date_epoch=$(git show -s --format=%ct HEAD)
tag="$version-$commit"
gochat_image="gochat/gochat:$tag"
connector_image="gochat/shangwutong:$tag"
output_dir=$(realpath -m "${GOCHAT_DIST_DIR:-$root/dist}")
[[ $output_dir != / && $output_dir != "$root" ]] || {
echo "invalid GOCHAT_DIST_DIR: $output_dir" >&2
exit 1
}
mkdir -p "$(dirname "$output_dir")"
stage=$(mktemp -d "$(dirname "$output_dir")/.gochat-dist.XXXXXX")
builder="gochat-dist-$$-$RANDOM"
buildkit_image='moby/buildkit:v0.13.2@sha256:9194b5ec1be368f41c516df7f93f7f540630ea06136056b2ffebb62226ed4ad6'
cleanup() {
[[ -z $builder ]] || docker buildx rm "$builder" >/dev/null 2>&1 || true
rm -rf "$stage"
}
trap cleanup EXIT HUP INT TERM
docker buildx create --name "$builder" --driver docker-container \
--driver-opt "image=$buildkit_image" >/dev/null
build_args=(
--build-arg "VERSION=$version"
--build-arg "COMMIT_SHA=$commit"
--build-arg "BUILD_DATE=$build_date"
--build-arg "SOURCE_DATE_EPOCH=$source_date_epoch"
)
build_image() {
local dockerfile=$1 image=$2 output=$3
docker buildx build --builder "$builder" --platform linux/amd64 \
"${build_args[@]}" --tag "$image" --file "$dockerfile" \
--output "type=docker,rewrite-timestamp=true,dest=$output" .
docker load --input "$output"
}
build_image deploy/docker/Dockerfile "$gochat_image" "$stage/gochat.image.tar"
build_image channels/shangwutong/Dockerfile "$connector_image" "$stage/shangwutong.image.tar"
docker image inspect "$gochat_image" "$connector_image" >/dev/null
docker buildx rm "$builder" >/dev/null
builder=
rm "$stage/gochat.image.tar" "$stage/shangwutong.image.tar"
archive="images/gochat-$tag-images.tgz"
mkdir -p "$stage/images"
docker save --output "$stage/images.tar" "$gochat_image" "$connector_image"
mkdir "$stage/.image-archive"
tar -xf "$stage/images.tar" -C "$stage/.image-archive"
mapfile -t image_archive_entries < <(find "$stage/.image-archive" -mindepth 1 -maxdepth 1 -printf '%f\n' | sort)
tar --sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner \
-cf "$stage/images.normalized.tar" -C "$stage/.image-archive" "${image_archive_entries[@]}"
rm -rf "$stage/.image-archive" "$stage/images.tar"
mv "$stage/images.normalized.tar" "$stage/images.tar"
gzip -n "$stage/images.tar"
mv "$stage/images.tar.gz" "$stage/$archive"
files=(
deploy/docker/docker-compose.prod.yml
deploy/docker/docker-compose.prod-smoke.yml
deploy/docker/database_client_entrypoint.sh
deploy/docker/database_host_rejection_cases.txt
deploy/docker/preflight.sh
deploy/fluentd/fluent.conf
deploy/prometheus/alertmanager.yml
deploy/prometheus/blackbox.yml
deploy/prometheus/postgres_queries.yml
deploy/prometheus/prometheus.yml
backend/configs/prometheus_alerts.yml
docs/ops/01-rolling-upgrade.md
docs/ops/02-production-operations.md
)
for file in "${files[@]}"; do
mkdir -p "$stage/$(dirname "$file")"
cp "$file" "$stage/$file"
done
sed \
-e "s|^GOCHAT_IMAGE_REF=.*|GOCHAT_IMAGE_REF=$gochat_image|" \
-e "s|^SHANGWUTONG_IMAGE_REF=.*|SHANGWUTONG_IMAGE_REF=$connector_image|" \
.env.example > "$stage/.env.example"
cp deploy/docker/README.dist.md "$stage/README.md"
printf 'SOURCE_COMMIT=%s\nGOCHAT_IMAGE_REF=%s\nSHANGWUTONG_IMAGE_REF=%s\n' \
"$(git rev-parse HEAD)" "$gochat_image" "$connector_image" > "$stage/MANIFEST"
(cd "$stage" && find . -type f ! -name SHA256SUMS -print | sort | xargs sha256sum > SHA256SUMS)
rm -rf "$output_dir"
mv "$stage" "$output_dir"
trap - EXIT HUP INT TERM
echo "Production bundle: $output_dir"
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
set -euo pipefail
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
test_tmp=$(mktemp -d)
dirty_file=
trap 'rm -rf "$test_tmp"; [[ -z $dirty_file ]] || rm -f "$dirty_file"' EXIT HUP INT TERM
real_docker=$(command -v docker)
output="$test_tmp/dist"
mkdir -p "$test_tmp/bin"
cat > "$test_tmp/bin/docker" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$*" >> "$DOCKER_LOG"
[[ $1 == buildx && $2 == create ]] && exit 0
[[ $1 == buildx && $2 == rm ]] && exit 0
if [[ $1 == buildx && $2 == build ]]; then
for arg in "$@"; do
[[ $arg == type=docker,*dest=* ]] && output=${arg##*dest=}
done
fake_archive=$(mktemp -d)
printf '[]\n' > "$fake_archive/manifest.json"
tar -cf "$output" -C "$fake_archive" manifest.json
rm -rf "$fake_archive"
exit 0
fi
[[ $1 == load && $2 == --input && -s $3 ]] && exit 0
[[ $1 == image && $2 == inspect ]] && exit 0
if [[ $1 == save && $2 == --output ]]; then
fake_archive=$(mktemp -d)
printf '[]\n' > "$fake_archive/manifest.json"
tar -cf "$3" -C "$fake_archive" manifest.json
rm -rf "$fake_archive"
exit 0
fi
exit 1
EOF
chmod +x "$test_tmp/bin/docker"
cd "$root"
dirty_file=$(mktemp "$root/.build-production-dirty.XXXXXX")
if PATH="$test_tmp/bin:$PATH" DOCKER_LOG="$test_tmp/docker.log" GOCHAT_DIST_DIR="$output" \
GOCHAT_VERSION=1.2.3 bash scripts/build-production.sh >"$test_tmp/dirty.out" 2>&1; then
echo 'dirty worktree build unexpectedly succeeded' >&2
exit 1
fi
grep -q 'refusing production build from a dirty worktree' "$test_tmp/dirty.out"
rm "$dirty_file"
dirty_file=
: > "$test_tmp/docker.log"
PATH="$test_tmp/bin:$PATH" DOCKER_LOG="$test_tmp/docker.log" GOCHAT_DIST_DIR="$output" \
GOCHAT_VERSION=1.2.3 bash scripts/build-production.sh
tag=1.2.3-$(git rev-parse --short=12 HEAD)
archive="$output/images/gochat-$tag-images.tgz"
test -s "$archive"
gzip -t "$archive"
(cd "$output" && sha256sum -c SHA256SUMS)
grep -q '^GOCHAT_IMAGE_REF=gochat/gochat:1.2.3-' "$output/.env.example"
grep -q '^SHANGWUTONG_IMAGE_REF=gochat/shangwutong:1.2.3-' "$output/.env.example"
test -f "$output/deploy/fluentd/fluent.conf"
test -f "$output/deploy/prometheus/prometheus.yml"
test -f "$output/backend/configs/prometheus_alerts.yml"
test "$(grep -c '^buildx build ' "$test_tmp/docker.log")" -eq 2
test "$(grep -c '^load --input ' "$test_tmp/docker.log")" -eq 2
test "$(grep -Fc -- "--build-arg VERSION=1.2.3 --build-arg COMMIT_SHA=$(git rev-parse --short=12 HEAD) --build-arg BUILD_DATE=$(git show -s --format=%cI HEAD) --build-arg SOURCE_DATE_EPOCH=$(git show -s --format=%ct HEAD)" "$test_tmp/docker.log")" -eq 2
grep -Fq "image inspect gochat/gochat:$tag gochat/shangwutong:$tag" "$test_tmp/docker.log"
test "$(grep -c '^save --output ' "$test_tmp/docker.log")" -eq 1
grep '^save --output ' "$test_tmp/docker.log" | \
grep -Fq " gochat/gochat:$tag gochat/shangwutong:$tag"
cp "$output/.env.example" "$output/.env"
(cd "$output" && "$real_docker" compose --env-file .env -f deploy/docker/docker-compose.prod.yml config --quiet)
rm "$output/.env"
first_sum=$(sha256sum "$archive")
PATH="$test_tmp/bin:$PATH" DOCKER_LOG="$test_tmp/docker.log" GOCHAT_DIST_DIR="$output" \
GOCHAT_VERSION=1.2.3 bash scripts/build-production.sh
test "$first_sum" = "$(sha256sum "$archive")"
echo 'production build script test passed'