package_offline.sh 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  4. # shellcheck source=wp13-common.sh
  5. source "${SCRIPT_DIR}/wp13-common.sh"
  6. usage() {
  7. cat <<'EOF'
  8. Usage: package_offline.sh [options]
  9. Build a checksum-verifiable DataOps Compose offline archive.
  10. Options:
  11. --repo-root PATH Repository root.
  12. --output-dir PATH Destination directory (default: <repo>/dist).
  13. --version VALUE Package version (default: source commit UTC timestamp).
  14. --source-date-epoch N Reproducible build timestamp (default: source commit time).
  15. --skip-images Contract-test mode; omit Docker image archive.
  16. --help Show this help.
  17. EOF
  18. }
  19. REPO_ROOT="$(wp13_default_root)"
  20. OUTPUT_DIR=""
  21. VERSION=""
  22. SOURCE_DATE_EPOCH=""
  23. SKIP_IMAGES=0
  24. while [[ $# -gt 0 ]]; do
  25. case "$1" in
  26. --repo-root) REPO_ROOT="$2"; shift 2 ;;
  27. --output-dir) OUTPUT_DIR="$2"; shift 2 ;;
  28. --version) VERSION="$2"; shift 2 ;;
  29. --source-date-epoch) SOURCE_DATE_EPOCH="$2"; shift 2 ;;
  30. --skip-images) SKIP_IMAGES=1; shift ;;
  31. --help|-h) usage; exit 0 ;;
  32. *) wp13_die "unknown argument: $1" ;;
  33. esac
  34. done
  35. REPO_ROOT="$(cd "${REPO_ROOT}" && pwd)"
  36. OUTPUT_DIR="${OUTPUT_DIR:-${REPO_ROOT}/dist}"
  37. mkdir -p "${OUTPUT_DIR}"
  38. OUTPUT_DIR="$(cd "${OUTPUT_DIR}" && pwd)"
  39. wp13_require_command python3
  40. SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git -C "${REPO_ROOT}" show -s --format=%ct HEAD 2>/dev/null || printf 0)}"
  41. [[ "${SOURCE_DATE_EPOCH}" =~ ^[0-9]+$ ]] \
  42. || wp13_die "--source-date-epoch must be a non-negative integer"
  43. 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}")}"
  44. for required in \
  45. deploy/docker/docker-compose.yml \
  46. deploy/docker/.env.example \
  47. database \
  48. migrations \
  49. deployment/environments \
  50. docs/architecture/OPENAPI.yaml \
  51. deployment/compose; do
  52. [[ -e "${REPO_ROOT}/${required}" ]] \
  53. || wp13_die "required release input is missing: ${required}"
  54. done
  55. if ! diff -qr \
  56. --exclude='__pycache__' \
  57. --exclude='*.pyc' \
  58. "${REPO_ROOT}/app" "${REPO_ROOT}/deployment/app" >/dev/null; then
  59. wp13_die "deployment/app is not synchronised with app"
  60. fi
  61. if ! diff -qr \
  62. --exclude='__pycache__' \
  63. --exclude='*.pyc' \
  64. "${REPO_ROOT}/migrations" "${REPO_ROOT}/deployment/migrations" >/dev/null; then
  65. wp13_die "deployment/migrations is not synchronised with migrations"
  66. fi
  67. PACKAGE_NAME="dataops-platform-offline-${VERSION}"
  68. TEMP_DIR="$(mktemp -d)"
  69. trap 'rm -rf "${TEMP_DIR}"' EXIT
  70. BUNDLE_ROOT="${TEMP_DIR}/${PACKAGE_NAME}"
  71. mkdir -p \
  72. "${BUNDLE_ROOT}/deploy" \
  73. "${BUNDLE_ROOT}/environments" \
  74. "${BUNDLE_ROOT}/operations" \
  75. "${BUNDLE_ROOT}/images" \
  76. "${BUNDLE_ROOT}/trace"
  77. cp -R "${REPO_ROOT}/deploy/docker" "${BUNDLE_ROOT}/deploy/docker"
  78. cp -R "${REPO_ROOT}/database" "${BUNDLE_ROOT}/database"
  79. cp -R "${REPO_ROOT}/migrations" "${BUNDLE_ROOT}/migrations"
  80. cp -R "${REPO_ROOT}/deployment/environments/." "${BUNDLE_ROOT}/environments/"
  81. cp -R "${REPO_ROOT}/deployment/compose/." "${BUNDLE_ROOT}/operations/"
  82. cp "${REPO_ROOT}/deployment/MANIFEST.md" "${BUNDLE_ROOT}/MANIFEST.md"
  83. cp "${REPO_ROOT}/docs/architecture/OPENAPI.yaml" "${BUNDLE_ROOT}/trace/OPENAPI.yaml"
  84. find "${BUNDLE_ROOT}" -type d -name __pycache__ -prune -exec rm -rf {} +
  85. find "${BUNDLE_ROOT}" -type f -name '*.pyc' -delete
  86. COMPOSE_FILE="${BUNDLE_ROOT}/deploy/docker/docker-compose.yml"
  87. IMAGE_LIST="${BUNDLE_ROOT}/images/images.txt"
  88. wp13_require_command docker
  89. docker compose -f "${REPO_ROOT}/deploy/docker/docker-compose.yml" \
  90. config --images | LC_ALL=C sort -u >"${IMAGE_LIST}"
  91. python3 "${REPO_ROOT}/deployment/compose/product_engineering.py" sbom \
  92. --repo-root "${REPO_ROOT}" \
  93. --output-dir "${BUNDLE_ROOT}/trace"
  94. python3 "${REPO_ROOT}/deployment/compose/product_engineering.py" manifest \
  95. --repo-root "${REPO_ROOT}" \
  96. --version "${VERSION}" \
  97. --source-date-epoch "${SOURCE_DATE_EPOCH}" \
  98. --output "${BUNDLE_ROOT}/release-manifest.json"
  99. 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}")"
  100. {
  101. printf 'version=%s\n' "${VERSION}"
  102. printf 'created_at_utc=%s\n' "${CREATED_AT_UTC}"
  103. printf 'source_date_epoch=%s\n' "${SOURCE_DATE_EPOCH}"
  104. printf 'source_commit=%s\n' "$(git -C "${REPO_ROOT}" rev-parse HEAD 2>/dev/null || printf unknown)"
  105. printf 'source_tree=%s\n' "$(git -C "${REPO_ROOT}" rev-parse 'HEAD^{tree}' 2>/dev/null || printf unknown)"
  106. printf 'compose_sha256=%s\n' "$(wp13_sha256_file "${COMPOSE_FILE}")"
  107. printf 'release_manifest_sha256=%s\n' "$(wp13_sha256_file "${BUNDLE_ROOT}/release-manifest.json")"
  108. printf 'openapi_sha256=%s\n' "$(wp13_sha256_file "${BUNDLE_ROOT}/trace/OPENAPI.yaml")"
  109. printf 'images_included=%s\n' "$([[ "${SKIP_IMAGES}" -eq 1 ]] && printf no || printf yes)"
  110. while IFS= read -r image; do
  111. [[ -n "${image}" ]] || continue
  112. image_id="$(docker image inspect -f '{{.Id}}' "${image}" 2>/dev/null || printf missing)"
  113. printf 'image=%s|%s\n' "${image}" "${image_id}"
  114. done <"${IMAGE_LIST}"
  115. } >"${BUNDLE_ROOT}/release-manifest.txt"
  116. if [[ "${SKIP_IMAGES}" -eq 0 ]]; then
  117. while IFS= read -r image; do
  118. [[ -n "${image}" ]] || continue
  119. docker image inspect "${image}" >/dev/null 2>&1 \
  120. || wp13_die "cannot package missing image: ${image}"
  121. done <"${IMAGE_LIST}"
  122. # shellcheck disable=SC2046
  123. docker save -o "${BUNDLE_ROOT}/images/dataops-images.tar" $(cat "${IMAGE_LIST}")
  124. fi
  125. wp13_write_checksums "${BUNDLE_ROOT}"
  126. ARCHIVE="${OUTPUT_DIR}/${PACKAGE_NAME}.tar.gz"
  127. python3 "${REPO_ROOT}/deployment/compose/reproducible_archive.py" \
  128. --source "${BUNDLE_ROOT}" \
  129. --output "${ARCHIVE}" \
  130. --source-date-epoch "${SOURCE_DATE_EPOCH}"
  131. wp13_log "offline package created: ${ARCHIVE}"