#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" RELEASE_ROOT="${DOMAINCHECK_RELEASE_ROOT:-${ROOT_DIR}/release}" TIMESTAMP="${DOMAINCHECK_RELEASE_TIMESTAMP:-$(date +%Y%m%d_%H%M%S)}" BASE_PACKAGE_NAME="domaincheck_release_${TIMESTAMP}" PACKAGE_NAME="${BASE_PACKAGE_NAME}" STAGING_ROOT="${RELEASE_ROOT}/${PACKAGE_NAME}" ARCHIVE_PATH="${RELEASE_ROOT}/${PACKAGE_NAME}.tar.gz" HASH_PATH="${RELEASE_ROOT}/${PACKAGE_NAME}.sha256.txt" LATEST_TXT_PATH="${RELEASE_ROOT}/latest_release.txt" LATEST_JSON_PATH="${RELEASE_ROOT}/latest_release.json" SMOKE_PYTHON="${DOMAINCHECK_SMOKE_PYTHON:-${ROOT_DIR}/domainCheck/.venv/bin/python}" SMOKE_RUNNER="${ROOT_DIR}/domain-api/deploy/linux/smoke_test.py" SMOKE_OUTPUT="${STAGING_ROOT}/smoke_test_report.json" API_BASE_URL="${API_BASE_URL:-http://127.0.0.1:8100}" WEB_URL="${WEB_URL:-}" SKIP_SMOKE_TEST="${DOMAINCHECK_SKIP_SMOKE_TEST:-0}" RELEASE_COMMIT_SHA="${DOMAINCHECK_RELEASE_COMMIT_SHA:-}" RELEASE_COMMIT_REF="${DOMAINCHECK_RELEASE_COMMIT_REF:-}" copy_optional_item() { local source_path="$1" local destination_path="$2" if [[ -e "${source_path}" ]]; then cp -a "${source_path}" "${destination_path}" fi } summarize_smoke_report() { local report_path="$1" python3 - "${report_path}" <<'PY' import json import sys from pathlib import Path report_path = Path(sys.argv[1]) if not report_path.exists(): print("false\treport missing") raise SystemExit(0) payload = json.loads(report_path.read_text(encoding="utf-8")) checks = [item for item in list(payload.get("checks") or []) if isinstance(item, dict)] total = len(checks) failed = [str(item.get("name") or "").strip() for item in checks if not bool(item.get("ok"))] passed = total - len(failed) ok = bool(payload.get("ok", False)) if ok: message = f"all checks passed ({passed}/{total})" if total else "all checks passed" else: failed_preview = ", ".join([name for name in failed if name][:5]) if failed_preview: message = f"{passed}/{total} checks passed; failed: {failed_preview}" else: message = f"{passed}/{total} checks passed" print(f"{str(ok).lower()}\t{message}") PY } mkdir -p "${RELEASE_ROOT}" PACKAGE_SUFFIX=1 while [[ -e "${STAGING_ROOT}" || -e "${ARCHIVE_PATH}" || -e "${HASH_PATH}" ]]; do PACKAGE_NAME="${BASE_PACKAGE_NAME}_${PACKAGE_SUFFIX}" STAGING_ROOT="${RELEASE_ROOT}/${PACKAGE_NAME}" ARCHIVE_PATH="${RELEASE_ROOT}/${PACKAGE_NAME}.tar.gz" HASH_PATH="${RELEASE_ROOT}/${PACKAGE_NAME}.sha256.txt" PACKAGE_SUFFIX=$((PACKAGE_SUFFIX + 1)) done if [[ -z "${RELEASE_COMMIT_SHA}" ]] && command -v git >/dev/null 2>&1; then if git -C "${ROOT_DIR}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then RELEASE_COMMIT_SHA="$(git -C "${ROOT_DIR}" rev-parse --short=12 HEAD 2>/dev/null || true)" RELEASE_COMMIT_REF="$(git -C "${ROOT_DIR}" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" fi fi rm -rf "${STAGING_ROOT}" rm -f "${ARCHIVE_PATH}" "${HASH_PATH}" mkdir -p "${STAGING_ROOT}" shopt -s nullglob DOCS_TARGET="${STAGING_ROOT}/docs" mkdir -p "${DOCS_TARGET}" DOC_FILES=("${ROOT_DIR}"/docs/*.md) if ((${#DOC_FILES[@]})); then cp -a "${DOC_FILES[@]}" "${DOCS_TARGET}/" fi SCRIPTS_TARGET="${STAGING_ROOT}/scripts" mkdir -p "${SCRIPTS_TARGET}" ROOT_SCRIPTS=( "start_domain_api.ps1" "stop_domain_api.ps1" "start_domain_web.ps1" "stop_domain_web.ps1" "start_domain_web_background.ps1" "start_domain_stack.ps1" "stop_domain_stack.ps1" "smoke_test_stack.ps1" "package_domain_release.ps1" "verify_domain_release.ps1" "prepare_final_release.ps1" "show_latest_release.ps1" "package_domain_release.sh" "verify_domain_release.sh" "prepare_final_release.sh" "show_latest_release.sh" ) for script_name in "${ROOT_SCRIPTS[@]}"; do copy_optional_item "${ROOT_DIR}/${script_name}" "${SCRIPTS_TARGET}/" done API_TARGET="${STAGING_ROOT}/domain-api" mkdir -p "${API_TARGET}" API_ITEMS=( "app" "deploy" "README.md" "requirements.txt" ".env.example" ) for item in "${API_ITEMS[@]}"; do copy_optional_item "${ROOT_DIR}/domain-api/${item}" "${API_TARGET}/" done WEB_TARGET="${STAGING_ROOT}/domain-web" mkdir -p "${WEB_TARGET}" WEB_ITEMS=( "src" "deploy" "README.md" "package.json" "package-lock.json" "tsconfig.json" "tsconfig.node.json" "vite.config.ts" ".env.example" ".env.production.example" ) for item in "${WEB_ITEMS[@]}"; do copy_optional_item "${ROOT_DIR}/domain-web/${item}" "${WEB_TARGET}/" done copy_optional_item "${ROOT_DIR}/domain-web/dist" "${WEB_TARGET}/" SMOKE_OK="false" SMOKE_MESSAGE="" if [[ "${SKIP_SMOKE_TEST}" == "1" ]]; then SMOKE_MESSAGE="skipped by env" elif [[ -x "${SMOKE_PYTHON}" && -f "${SMOKE_RUNNER}" ]]; then if smoke_log="$("${SMOKE_PYTHON}" "${SMOKE_RUNNER}" --base-url "${API_BASE_URL}" --web-url "${WEB_URL}" --output "${SMOKE_OUTPUT}" 2>&1)"; then if [[ -f "${SMOKE_OUTPUT}" ]]; then SMOKE_OK="true" SMOKE_MESSAGE="generated" else SMOKE_MESSAGE="smoke test succeeded but report missing" fi else SMOKE_MESSAGE="${smoke_log}" fi else SMOKE_MESSAGE="smoke test runtime not found" fi if [[ -f "${SMOKE_OUTPUT}" ]]; then SMOKE_SUMMARY="$(summarize_smoke_report "${SMOKE_OUTPUT}")" SMOKE_OK="${SMOKE_SUMMARY%%$'\t'*}" SMOKE_MESSAGE="${SMOKE_SUMMARY#*$'\t'}" fi python3 - "${STAGING_ROOT}" "${PACKAGE_NAME}" "${ROOT_DIR}" "${SMOKE_OK}" "${SMOKE_MESSAGE}" "${API_BASE_URL}" "${WEB_URL}" "${RELEASE_COMMIT_SHA}" "${RELEASE_COMMIT_REF}" <<'PY' import json import os import sys from datetime import datetime from pathlib import Path staging_root = Path(sys.argv[1]) package_name = sys.argv[2] root_dir = sys.argv[3] smoke_ok = sys.argv[4].lower() == "true" smoke_message = sys.argv[5] api_base_url = sys.argv[6] web_url = sys.argv[7] commit_sha = sys.argv[8] commit_ref = sys.argv[9] docs_target = staging_root / "docs" scripts_target = staging_root / "scripts" api_target = staging_root / "domain-api" web_target = staging_root / "domain-web" smoke_output = staging_root / "smoke_test_report.json" manifest = { "package_name": package_name, "package_type": "tar.gz", "generated_at": datetime.now().isoformat(timespec="seconds"), "root": root_dir, "commit_sha": commit_sha, "commit_ref": commit_ref, "smoke_test": { "ok": smoke_ok, "message": smoke_message, "api_base_url": api_base_url, "web_url": web_url, "report": "smoke_test_report.json" if smoke_output.exists() else "", }, "included": { "docs": sorted(item.name for item in docs_target.iterdir() if item.is_file()) if docs_target.exists() else [], "scripts": sorted(item.name for item in scripts_target.iterdir() if item.is_file()) if scripts_target.exists() else [], "domain_api": sorted(item.name for item in api_target.iterdir()) if api_target.exists() else [], "domain_web": sorted(item.name for item in web_target.iterdir()) if web_target.exists() else [], }, } (staging_root / "release_manifest.json").write_text( json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8", ) PY cat > "${STAGING_ROOT}/README_RELEASE.txt" < "${HASH_PATH}" <