#!/usr/bin/env bash set -Eeuo pipefail project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" domain="approval.justconn.top" public_base_url="https://${domain}" gateway_url="${public_base_url}/mcp" public_download_base_url="${public_base_url}/downloads/dingmcp" gitea_repo="gitea-admin/dingMCP" release_dir="${project_root}/release" web_base="/var/www/${domain}" web_root="${web_base}/downloads/dingmcp" web_releases="${web_base}/releases" acme_root="${web_base}/acme" nginx_site="/etc/nginx/conf.d/${domain}.conf" certificate="/etc/letsencrypt/live/${domain}/fullchain.pem" npm_cache="${project_root}/.npm-cache" release_private_key="${project_root}/.secrets/release-signing-private.pem" release_public_key="${project_root}/distribution/release-signing-public.pem" for command_name in node npm curl systemctl sudo nginx certbot stat sha256sum tar find sort tail cut date git tea; do command -v "${command_name}" >/dev/null 2>&1 || { echo "缺少部署命令:${command_name}" >&2 exit 1 } done cd "${project_root}" release_version="$(node -p "require('./package.json').version")" release_tag="v${release_version}" gitea_release_base_url="https://git.justconn.top/${gitea_repo}/releases/download/${release_tag}" if [[ "$(git branch --show-current)" != "main" ]] || ! git diff --quiet || ! git diff --cached --quiet; then echo "正式部署只能从无未提交改动的 main 分支执行。" >&2 exit 1 fi release_commit="$(git rev-parse HEAD)" remote_main="$(git ls-remote origin refs/heads/main | cut -f1)" if [[ -z "${remote_main}" || "${remote_main}" != "${release_commit}" ]]; then echo "当前 main 提交尚未同步到 Gitea origin/main,拒绝创建发布标签。" >&2 exit 1 fi release_tag_commit="$(git ls-remote origin "refs/tags/${release_tag}" "refs/tags/${release_tag}^{}" | tail -n1 | cut -f1)" release_created_at_commit="${release_tag_commit:-${release_commit}}" if ! git cat-file -e "${release_created_at_commit}^{commit}" 2>/dev/null; then echo "现有发布标签指向本地不可验证的提交,拒绝重新生成同版本发布物。" >&2 exit 1 fi release_created_at="$(git show -s --format=%cI "${release_created_at_commit}")" if [[ ! -f "${project_root}/.runtime/dingtalk-mcp.env" ]]; then echo "缺少 .runtime/dingtalk-mcp.env;首次部署前需先配置网关企业凭证。" >&2 exit 1 fi if [[ ! -f "${release_private_key}" || ! -f "${release_public_key}" ]]; then echo "缺少 Ed25519 发布密钥;首次部署请先执行 npm run release:keygen 并安全备份私钥。" >&2 exit 1 fi if [[ "$(stat -c '%a' "${release_private_key}")" != "600" ]]; then echo "发布私钥权限必须为 600:${release_private_key}" >&2 exit 1 fi echo "[1/8] 测试并构建" npm --cache "${npm_cache}" test npm run runtime:verify echo "[2/8] 生成包含钉钉 OA 与薪福通的发行包" npm --cache "${npm_cache}" run release:build -- \ --gateway-url "${gateway_url}" \ --download-base-url "${gitea_release_base_url}" \ --download-mirror-base-url "${public_download_base_url}" \ --created-at "${release_created_at}" \ --out release npm run test-bundle:build -- \ --release-dir release \ --bundle release/dingmcp-test-bundle.tar.gz \ --bundle-url "${public_download_base_url}/dingmcp-test-bundle.tar.gz" \ --prompt release/agent-test-install.md (cd "${release_dir}" && sha256sum -c SHA256SUMS) npm run release:verify -- --release-dir release echo "[3/8] 发布或复用 Gitea 国内首选下载源" gitea_releases_json="$(mktemp)" trap 'rm -f "${gitea_releases_json}"' EXIT tea releases list --remote origin --limit 100 --output json >"${gitea_releases_json}" if node scripts/release-tag-exists.mjs "${release_tag}" <"${gitea_releases_json}"; then : else release_lookup_status=$? if [[ "${release_lookup_status}" -ne 1 ]]; then echo "无法可靠判断 Gitea Release 是否存在,拒绝继续部署。" >&2 exit "${release_lookup_status}" fi release_flags=() [[ "${release_version}" == *-* ]] && release_flags+=(--prerelease) tea releases create --remote origin --tag "${release_tag}" --target "${release_commit}" \ --title "${release_tag}" --note "Signed DingTalk approval client ${release_version}" "${release_flags[@]}" fi mapfile -t mirror_assets < <(find "${release_dir}" -mindepth 1 -maxdepth 1 -type f \ ! -name 'agent-test-install.md' ! -name 'dingmcp-test-bundle.tar.gz' -print | sort) missing_mirror_assets=() mirror_check_file="$(mktemp)" for asset in "${mirror_assets[@]}"; do asset_name="$(basename "${asset}")" if curl --fail --silent --location "${gitea_release_base_url}/${asset_name}" -o "${mirror_check_file}" 2>/dev/null; then if [[ "$(sha256sum "${mirror_check_file}" | cut -d' ' -f1)" != "$(sha256sum "${asset}" | cut -d' ' -f1)" ]]; then echo "Gitea 已存在同版本但内容不同的发布物:${asset_name};请提升版本号后重新发布。" >&2 exit 1 fi else missing_mirror_assets+=("${asset}") fi done if ((${#missing_mirror_assets[@]} > 0)); then tea releases assets create --remote origin "${release_tag}" "${missing_mirror_assets[@]}" fi for asset in "${mirror_assets[@]}"; do asset_name="$(basename "${asset}")" curl --fail --silent --show-error --location "${gitea_release_base_url}/${asset_name}" -o "${mirror_check_file}" [[ "$(sha256sum "${mirror_check_file}" | cut -d' ' -f1)" == "$(sha256sum "${asset}" | cut -d' ' -f1)" ]] || { echo "Gitea 首选发布物校验失败:${asset_name}" >&2 exit 1 } done rm -f "${mirror_check_file}" "${gitea_releases_json}" trap - EXIT echo "[4/8] 安装并重启用户级网关服务" node dist/src/admin.js service start --project-root "${project_root}" echo "[5/8] 原子发布安装文件" release_digest="$(sha256sum "${release_dir}/manifest.json" | cut -c1-12)" release_name="${release_version}-${release_digest}" release_target="${web_releases}/${release_name}" release_staging="${web_releases}/.staging-${release_name}-$$" sudo install -d -m 0755 "${web_base}/downloads" "${web_releases}" "${acme_root}/.well-known/acme-challenge" sudo rm -rf "${release_staging}" sudo install -d -m 0755 "${release_staging}" sudo cp -a "${release_dir}/." "${release_staging}/" if ! sudo test -e "${release_target}"; then sudo mv "${release_staging}" "${release_target}" else sudo rm -rf "${release_staging}" fi if sudo test -d "${web_root}" && ! sudo test -L "${web_root}"; then sudo mv "${web_root}" "${web_releases}/legacy-$(date -u +%Y%m%dT%H%M%SZ)" fi sudo ln -sfn "${release_target}" "${web_root}.next" sudo mv -Tf "${web_root}.next" "${web_root}" mapfile -t obsolete_releases < <(find "${web_releases}" -mindepth 1 -maxdepth 1 -type d -name '[0-9]*' ! -name "${release_name}" -printf '%T@ %f\n' | sort -nr | tail -n +3 | cut -d' ' -f2-) for obsolete in "${obsolete_releases[@]}"; do [[ "${obsolete}" =~ ^[A-Za-z0-9._-]+$ ]] || continue sudo rm -rf "${web_releases:?}/${obsolete}" done if ! sudo test -f "${certificate}"; then echo "[6/8] 首次签发 HTTPS 证书" sudo install -m 0644 "${project_root}/deploy/${domain}.bootstrap.nginx.conf" "${nginx_site}" sudo nginx -t sudo systemctl reload nginx sudo certbot certonly --webroot -w "${acme_root}" -d "${domain}" else echo "[6/8] 复用现有 HTTPS 证书" fi echo "[7/8] 安装正式 Nginx 配置" sudo install -m 0644 "${project_root}/deploy/${domain}.nginx.conf" "${nginx_site}" sudo nginx -t sudo systemctl reload nginx echo "[8/8] 验证公网服务与备用发布源" curl --fail --silent --show-error --retry 10 --retry-delay 1 --retry-all-errors \ "${public_base_url}/healthz" >/dev/null curl --fail --silent --show-error \ "${public_base_url}/.well-known/oauth-protected-resource/mcp" >/dev/null curl --fail --silent --show-error "${public_download_base_url}/install.md" >/dev/null curl --fail --silent --show-error "${public_download_base_url}/compat-install.md" >/dev/null curl --fail --silent --show-error "${public_download_base_url}/manifest.json" >/dev/null curl --fail --silent --show-error "${public_download_base_url}/manifest.sig" >/dev/null curl --fail --silent --show-error "${public_download_base_url}/dingtalk-approval-installer.mjs" >/dev/null curl --fail --silent --show-error "${public_download_base_url}/release-signing-public.pem" >/dev/null curl --fail --silent --show-error --location "${gitea_release_base_url}/manifest.json" >/dev/null curl --fail --silent --show-error --location "${gitea_release_base_url}/manifest.sig" >/dev/null echo "部署完成。" echo "MCP:${gateway_url}" echo "用户安装链接:${public_download_base_url}/install.md" echo "国内静态发布源:${gitea_release_base_url}/install.md" echo "黑盒测试提示词:${public_download_base_url}/agent-test-install.md"