#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=wp13-common.sh source "${SCRIPT_DIR}/wp13-common.sh" usage() { cat <<'EOF' Usage: package_offline.sh [options] Build a checksum-verifiable DataOps Compose offline archive. Options: --repo-root PATH Repository root. --output-dir PATH Destination directory (default: /dist). --version VALUE Package version (default: source commit UTC timestamp). --source-date-epoch N Reproducible build timestamp (default: source commit time). --skip-images Contract-test mode; omit Docker image archive. --help Show this help. EOF } REPO_ROOT="$(wp13_default_root)" OUTPUT_DIR="" VERSION="" SOURCE_DATE_EPOCH="" SKIP_IMAGES=0 while [[ $# -gt 0 ]]; do case "$1" in --repo-root) REPO_ROOT="$2"; shift 2 ;; --output-dir) OUTPUT_DIR="$2"; shift 2 ;; --version) VERSION="$2"; shift 2 ;; --source-date-epoch) SOURCE_DATE_EPOCH="$2"; shift 2 ;; --skip-images) SKIP_IMAGES=1; shift ;; --help|-h) usage; exit 0 ;; *) wp13_die "unknown argument: $1" ;; esac done REPO_ROOT="$(cd "${REPO_ROOT}" && pwd)" OUTPUT_DIR="${OUTPUT_DIR:-${REPO_ROOT}/dist}" mkdir -p "${OUTPUT_DIR}" OUTPUT_DIR="$(cd "${OUTPUT_DIR}" && pwd)" wp13_require_command python3 SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git -C "${REPO_ROOT}" show -s --format=%ct HEAD 2>/dev/null || printf 0)}" [[ "${SOURCE_DATE_EPOCH}" =~ ^[0-9]+$ ]] \ || wp13_die "--source-date-epoch must be a non-negative integer" VERSION="${VERSION:-$(python3 -c 'import datetime,sys; print(datetime.datetime.fromtimestamp(int(sys.argv[1]), datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ"))' "${SOURCE_DATE_EPOCH}")}" for required in \ deploy/docker/docker-compose.yml \ deploy/docker/.env.example \ database \ migrations \ deployment/environments \ deployment/helm/dataops-platform \ deployment/enterprise \ docs/architecture/OPENAPI.yaml \ deployment/compose; do [[ -e "${REPO_ROOT}/${required}" ]] \ || wp13_die "required release input is missing: ${required}" done if ! diff -qr \ --exclude='__pycache__' \ --exclude='*.pyc' \ "${REPO_ROOT}/app" "${REPO_ROOT}/deployment/app" >/dev/null; then wp13_die "deployment/app is not synchronised with app" fi if ! diff -qr \ --exclude='__pycache__' \ --exclude='*.pyc' \ "${REPO_ROOT}/migrations" "${REPO_ROOT}/deployment/migrations" >/dev/null; then wp13_die "deployment/migrations is not synchronised with migrations" fi PACKAGE_NAME="dataops-platform-offline-${VERSION}" TEMP_DIR="$(mktemp -d)" trap 'rm -rf "${TEMP_DIR}"' EXIT BUNDLE_ROOT="${TEMP_DIR}/${PACKAGE_NAME}" mkdir -p \ "${BUNDLE_ROOT}/deploy" \ "${BUNDLE_ROOT}/environments" \ "${BUNDLE_ROOT}/operations" \ "${BUNDLE_ROOT}/helm" \ "${BUNDLE_ROOT}/enterprise" \ "${BUNDLE_ROOT}/images" \ "${BUNDLE_ROOT}/trace" cp -R "${REPO_ROOT}/deploy/docker" "${BUNDLE_ROOT}/deploy/docker" cp -R "${REPO_ROOT}/database" "${BUNDLE_ROOT}/database" cp -R "${REPO_ROOT}/migrations" "${BUNDLE_ROOT}/migrations" cp -R "${REPO_ROOT}/deployment/environments/." "${BUNDLE_ROOT}/environments/" cp -R "${REPO_ROOT}/deployment/compose/." "${BUNDLE_ROOT}/operations/" cp -R "${REPO_ROOT}/deployment/helm/dataops-platform" "${BUNDLE_ROOT}/helm/dataops-platform" cp -R "${REPO_ROOT}/deployment/enterprise/." "${BUNDLE_ROOT}/enterprise/" cp "${REPO_ROOT}/deployment/MANIFEST.md" "${BUNDLE_ROOT}/MANIFEST.md" cp "${REPO_ROOT}/docs/architecture/OPENAPI.yaml" "${BUNDLE_ROOT}/trace/OPENAPI.yaml" find "${BUNDLE_ROOT}" -type d -name __pycache__ -prune -exec rm -rf {} + find "${BUNDLE_ROOT}" -type f -name '*.pyc' -delete COMPOSE_FILE="${BUNDLE_ROOT}/deploy/docker/docker-compose.yml" IMAGE_LIST="${BUNDLE_ROOT}/images/images.txt" wp13_require_command docker docker compose -f "${REPO_ROOT}/deploy/docker/docker-compose.yml" \ config --images | LC_ALL=C sort -u >"${IMAGE_LIST}" python3 "${REPO_ROOT}/deployment/compose/product_engineering.py" sbom \ --repo-root "${REPO_ROOT}" \ --output-dir "${BUNDLE_ROOT}/trace" python3 "${REPO_ROOT}/deployment/compose/product_engineering.py" manifest \ --repo-root "${REPO_ROOT}" \ --version "${VERSION}" \ --source-date-epoch "${SOURCE_DATE_EPOCH}" \ --output "${BUNDLE_ROOT}/release-manifest.json" CREATED_AT_UTC="$(python3 -c 'import datetime,sys; print(datetime.datetime.fromtimestamp(int(sys.argv[1]), datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"))' "${SOURCE_DATE_EPOCH}")" { printf 'version=%s\n' "${VERSION}" printf 'created_at_utc=%s\n' "${CREATED_AT_UTC}" printf 'source_date_epoch=%s\n' "${SOURCE_DATE_EPOCH}" printf 'source_commit=%s\n' "$(git -C "${REPO_ROOT}" rev-parse HEAD 2>/dev/null || printf unknown)" printf 'source_tree=%s\n' "$(git -C "${REPO_ROOT}" rev-parse 'HEAD^{tree}' 2>/dev/null || printf unknown)" printf 'compose_sha256=%s\n' "$(wp13_sha256_file "${COMPOSE_FILE}")" printf 'release_manifest_sha256=%s\n' "$(wp13_sha256_file "${BUNDLE_ROOT}/release-manifest.json")" printf 'openapi_sha256=%s\n' "$(wp13_sha256_file "${BUNDLE_ROOT}/trace/OPENAPI.yaml")" printf 'images_included=%s\n' "$([[ "${SKIP_IMAGES}" -eq 1 ]] && printf no || printf yes)" while IFS= read -r image; do [[ -n "${image}" ]] || continue image_id="$(docker image inspect -f '{{.Id}}' "${image}" 2>/dev/null || printf missing)" printf 'image=%s|%s\n' "${image}" "${image_id}" done <"${IMAGE_LIST}" } >"${BUNDLE_ROOT}/release-manifest.txt" if [[ "${SKIP_IMAGES}" -eq 0 ]]; then while IFS= read -r image; do [[ -n "${image}" ]] || continue docker image inspect "${image}" >/dev/null 2>&1 \ || wp13_die "cannot package missing image: ${image}" done <"${IMAGE_LIST}" # shellcheck disable=SC2046 docker save -o "${BUNDLE_ROOT}/images/dataops-images.tar" $(cat "${IMAGE_LIST}") fi wp13_write_checksums "${BUNDLE_ROOT}" ARCHIVE="${OUTPUT_DIR}/${PACKAGE_NAME}.tar.gz" python3 "${REPO_ROOT}/deployment/compose/reproducible_archive.py" \ --source "${BUNDLE_ROOT}" \ --output "${ARCHIVE}" \ --source-date-epoch "${SOURCE_DATE_EPOCH}" wp13_log "offline package created: ${ARCHIVE}"