#!/usr/bin/env bash set -euo pipefail WP13_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" export COMPOSE_PROGRESS="${COMPOSE_PROGRESS:-plain}" wp13_log() { printf '[WP13] %s\n' "$*" } wp13_die() { printf '[WP13] ERROR: %s\n' "$*" >&2 exit 1 } wp13_require_command() { command -v "$1" >/dev/null 2>&1 || wp13_die "required command not found: $1" } wp13_default_root() { if [[ -f "${WP13_SCRIPT_DIR}/../../deploy/docker/docker-compose.yml" ]]; then (cd "${WP13_SCRIPT_DIR}/../.." && pwd) return fi if [[ -f "${WP13_SCRIPT_DIR}/../deploy/docker/docker-compose.yml" ]]; then (cd "${WP13_SCRIPT_DIR}/.." && pwd) return fi (cd "${WP13_SCRIPT_DIR}/.." && pwd) } wp13_sha256_file() { local file="$1" if command -v sha256sum >/dev/null 2>&1; then sha256sum "${file}" | awk '{print $1}' else shasum -a 256 "${file}" | awk '{print $1}' fi } wp13_write_checksums() { local root="$1" local output="${root}/checksums.sha256" : >"${output}" while IFS= read -r relative; do printf '%s %s\n' \ "$(wp13_sha256_file "${root}/${relative}")" \ "${relative}" >>"${output}" done < <( cd "${root}" find . -type f \ ! -name checksums.sha256 \ ! -path '*/__pycache__/*' \ ! -name '*.pyc' \ -print \ | sed 's#^\./##' \ | LC_ALL=C sort ) } wp13_verify_checksums() { local root="$1" [[ -f "${root}/checksums.sha256" ]] \ || wp13_die "checksum manifest not found: ${root}/checksums.sha256" if command -v sha256sum >/dev/null 2>&1; then (cd "${root}" && sha256sum -c checksums.sha256 >/dev/null) \ || wp13_die "checksum verification failed" else (cd "${root}" && shasum -a 256 -c checksums.sha256 >/dev/null) \ || wp13_die "checksum verification failed" fi wp13_log "checksum verification passed" } wp13_compose() { local compose_file="$1" local project_name="$2" local env_file="$3" shift 3 if [[ -n "${env_file}" ]]; then docker compose \ --env-file "${env_file}" \ -p "${project_name}" \ -f "${compose_file}" \ "$@" else docker compose \ -p "${project_name}" \ -f "${compose_file}" \ "$@" fi } wp13_project_container_ids() { local project_name="$1" docker ps -aq \ --filter "label=com.docker.compose.project=${project_name}" } wp13_project_running_ids() { local project_name="$1" docker ps -q \ --filter "label=com.docker.compose.project=${project_name}" } wp13_assert_no_project_containers() { local project_name="$1" if [[ -n "$(wp13_project_container_ids "${project_name}")" ]]; then wp13_die "destination project already exists: ${project_name}" fi } wp13_wait_for_project() { local project_name="$1" local timeout_seconds="${2:-300}" local started_at started_at="$(date +%s)" while :; do local ids ids="$(wp13_project_container_ids "${project_name}")" [[ -n "${ids}" ]] || wp13_die "project has no containers: ${project_name}" local pending=0 local failed=0 local container_id for container_id in ${ids}; do local state health exit_code state="$(docker inspect -f '{{.State.Status}}' "${container_id}")" health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "${container_id}")" exit_code="$(docker inspect -f '{{.State.ExitCode}}' "${container_id}")" if [[ "${state}" == "exited" && "${exit_code}" == "0" ]]; then continue fi if [[ "${state}" == "exited" || "${state}" == "dead" ]]; then failed=1 break fi if [[ "${state}" != "running" ]]; then pending=1 continue fi if [[ "${health}" != "none" && "${health}" != "healthy" ]]; then pending=1 fi done [[ "${failed}" -eq 0 ]] || wp13_die "project contains a failed container: ${project_name}" if [[ "${pending}" -eq 0 ]]; then wp13_log "project is healthy: ${project_name}" return 0 fi if (( $(date +%s) - started_at >= timeout_seconds )); then wp13_die "project health timeout after ${timeout_seconds}s: ${project_name}" fi sleep 5 done } wp13_safe_archive_name() { printf '%s' "$1" | tr -c 'A-Za-z0-9._-' '_' } wp13_expected_migration_head() { local root="$1" local manifest="${root}/release-manifest.json" if [[ ! -f "${manifest}" ]]; then printf '' return fi wp13_require_command python3 python3 -c 'import json,sys; print(json.load(open(sys.argv[1], encoding="utf-8"))["migration"]["head"])' "${manifest}" }