diff --git a/.gitea/workflows/release-offline.yml b/.gitea/workflows/release-offline.yml new file mode 100644 index 0000000..6fe2211 --- /dev/null +++ b/.gitea/workflows/release-offline.yml @@ -0,0 +1,101 @@ +name: offline-release + +on: + workflow_dispatch: + inputs: + base_ref: + description: "可选:用于 impact diff 的起始 ref,留空则默认 HEAD^" + required: false + include_infra: + description: "是否同时打 infra bundle" + required: false + default: "false" + +jobs: + package-and-deploy: + runs-on: build-host + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Resolve release refs + shell: bash + run: | + set -euo pipefail + APP_TAG="$(git rev-parse --short=12 HEAD)" + BASE_REF="${{ inputs.base_ref }}" + if [[ -z "${BASE_REF}" ]] && git rev-parse --verify --quiet HEAD^ >/dev/null; then + BASE_REF="$(git rev-parse HEAD^)" + fi + { + echo "APP_TAG=${APP_TAG}" + echo "BASE_REF=${BASE_REF}" + } >> "${GITHUB_ENV}" + + - name: Build release plan + shell: bash + run: | + set -euo pipefail + ./deploy/impact-rules.sh "${BASE_REF:-}" HEAD deploy/release-plan.env + cat deploy/release-plan.env + + - name: Pack docker images + shell: bash + run: | + set -euo pipefail + source deploy/release-plan.env + args=(--app-tag "${APP_TAG}") + if [[ "${SMARTFLOW_BUILD_BACKEND}" != "1" ]]; then + args+=(--skip-backend) + fi + if [[ "${SMARTFLOW_BUILD_FRONTEND}" != "1" ]]; then + args+=(--skip-frontend) + fi + if [[ "${{ inputs.include_infra }}" == "true" ]]; then + args+=(--include-infra) + fi + ./deploy/docker-pack.sh "${args[@]}" + + - name: Stage release directory + shell: bash + run: | + set -euo pipefail + ./deploy/stage-release.sh \ + --release-dir ".release/${APP_TAG}" \ + --plan-file "deploy/release-plan.env" \ + --bundle-dir ".docker-bundles" + + - name: Upload release bundle + shell: bash + env: + SMARTFLOW_DEPLOY_HOST: ${{ secrets.SMARTFLOW_DEPLOY_HOST }} + SMARTFLOW_DEPLOY_PORT: ${{ secrets.SMARTFLOW_DEPLOY_PORT }} + SMARTFLOW_DEPLOY_USER: ${{ secrets.SMARTFLOW_DEPLOY_USER }} + SMARTFLOW_DEPLOY_SSH_KEY: ${{ secrets.SMARTFLOW_DEPLOY_SSH_KEY }} + run: | + set -euo pipefail + mkdir -p ~/.ssh + printf '%s\n' "${SMARTFLOW_DEPLOY_SSH_KEY}" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan -p "${SMARTFLOW_DEPLOY_PORT:-22}" "${SMARTFLOW_DEPLOY_HOST}" >> ~/.ssh/known_hosts + tar -C ".release/${APP_TAG}" -czf ".release/${APP_TAG}.tgz" . + ssh -p "${SMARTFLOW_DEPLOY_PORT:-22}" "${SMARTFLOW_DEPLOY_USER}@${SMARTFLOW_DEPLOY_HOST}" "mkdir -p /srv/smartflow/releases/${APP_TAG}" + scp -P "${SMARTFLOW_DEPLOY_PORT:-22}" ".release/${APP_TAG}.tgz" "${SMARTFLOW_DEPLOY_USER}@${SMARTFLOW_DEPLOY_HOST}:/srv/smartflow/releases/${APP_TAG}.tgz" + ssh -p "${SMARTFLOW_DEPLOY_PORT:-22}" "${SMARTFLOW_DEPLOY_USER}@${SMARTFLOW_DEPLOY_HOST}" "rm -rf /srv/smartflow/releases/${APP_TAG}/* && tar -xzf /srv/smartflow/releases/${APP_TAG}.tgz -C /srv/smartflow/releases/${APP_TAG} && rm -f /srv/smartflow/releases/${APP_TAG}.tgz" + + - name: Trigger deploy + shell: bash + env: + SMARTFLOW_DEPLOY_HOST: ${{ secrets.SMARTFLOW_DEPLOY_HOST }} + SMARTFLOW_DEPLOY_PORT: ${{ secrets.SMARTFLOW_DEPLOY_PORT }} + SMARTFLOW_DEPLOY_USER: ${{ secrets.SMARTFLOW_DEPLOY_USER }} + SMARTFLOW_DEPLOY_SSH_KEY: ${{ secrets.SMARTFLOW_DEPLOY_SSH_KEY }} + run: | + set -euo pipefail + mkdir -p ~/.ssh + printf '%s\n' "${SMARTFLOW_DEPLOY_SSH_KEY}" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan -p "${SMARTFLOW_DEPLOY_PORT:-22}" "${SMARTFLOW_DEPLOY_HOST}" >> ~/.ssh/known_hosts + ssh -p "${SMARTFLOW_DEPLOY_PORT:-22}" "${SMARTFLOW_DEPLOY_USER}@${SMARTFLOW_DEPLOY_HOST}" "smartflow-release deploy ${APP_TAG}" diff --git a/.gitignore b/.gitignore index 67f5806..99d0918 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ backend/config.yaml /backend/.dev/ /.docker-bundles/ .gopath/ +/deploy/release-plan.env +/.release/ diff --git a/deploy/certs/README.md b/deploy/certs/README.md index ea9b2bf..e6748c5 100644 --- a/deploy/certs/README.md +++ b/deploy/certs/README.md @@ -4,12 +4,15 @@ - `tls.crt`:证书链文件(如 `fullchain.pem` / `bundle.pem`) - `tls.key`:私钥文件 +- `git.lecspace.com_bundle.pem`:Gitea 域名 `git.lecspace.com` 的证书链 +- `git.lecspace.com.key`:Gitea 域名 `git.lecspace.com` 的私钥 `docker-compose.full.yml` 会把本目录挂载到前端容器的 `/etc/nginx/certs`, -`frontend/nginx.conf` 也会固定从该路径读取这两个文件。 +`frontend/nginx.conf` 与 `deploy/nginx/default.conf` 也会固定从该路径读取这些证书文件。 注意: 1. 证书和私钥属于敏感文件,不要提交到仓库。 2. 若证书文件名不同,请在部署前重命名为上面的通用名称。 3. 更新证书后,只需要重建或重启 `frontend` 容器即可生效。 +4. 如果暂时不接入 Gitea 域名,可先不放 `git.lecspace.com_bundle.pem` 与 `git.lecspace.com.key`,但对应站点会无法完成 HTTPS 代理。 diff --git a/deploy/docker-pack.sh b/deploy/docker-pack.sh new file mode 100755 index 0000000..d5e4278 --- /dev/null +++ b/deploy/docker-pack.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP_TAG="latest" +BACKEND_IMAGE="smartflow/backend-suite" +FRONTEND_IMAGE="smartflow/frontend" +OUTPUT_DIR=".docker-bundles" +INCLUDE_INFRA=0 +SKIP_BACKEND=0 +SKIP_FRONTEND=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --app-tag) + APP_TAG="$2" + shift 2 + ;; + --backend-image) + BACKEND_IMAGE="$2" + shift 2 + ;; + --frontend-image) + FRONTEND_IMAGE="$2" + shift 2 + ;; + --output-dir) + OUTPUT_DIR="$2" + shift 2 + ;; + --include-infra) + INCLUDE_INFRA=1 + shift + ;; + --skip-backend) + SKIP_BACKEND=1 + shift + ;; + --skip-frontend) + SKIP_FRONTEND=1 + shift + ;; + *) + echo "unknown argument: $1" >&2 + exit 64 + ;; + esac +done + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +bundle_dir="${repo_root}/${OUTPUT_DIR}" +backend_ref="${BACKEND_IMAGE}:${APP_TAG}" +frontend_ref="${FRONTEND_IMAGE}:${APP_TAG}" +app_bundle_path="${bundle_dir}/smartflow-app-${APP_TAG}.tar" +infra_bundle_path="${bundle_dir}/smartflow-infra-${APP_TAG}.tar" + +mkdir -p "${bundle_dir}" + +if [[ "${SKIP_BACKEND}" -eq 0 ]]; then + echo "==> Build backend image ${backend_ref}" + docker build --platform linux/amd64 -f "${repo_root}/backend/Dockerfile" -t "${backend_ref}" "${repo_root}/backend" +fi + +if [[ "${SKIP_FRONTEND}" -eq 0 ]]; then + echo "==> Build frontend image ${frontend_ref}" + docker build --platform linux/amd64 -f "${repo_root}/frontend/Dockerfile" -t "${frontend_ref}" "${repo_root}/frontend" +fi + +declare -a app_images=() +if [[ "${SKIP_BACKEND}" -eq 0 ]]; then + app_images+=("${backend_ref}") +fi +if [[ "${SKIP_FRONTEND}" -eq 0 ]]; then + app_images+=("${frontend_ref}") +fi + +if [[ "${#app_images[@]}" -gt 0 ]]; then + rm -f "${app_bundle_path}" + echo "==> Export app bundle to ${app_bundle_path}" + docker save -o "${app_bundle_path}" "${app_images[@]}" +else + echo "==> Skip app bundle export because backend/frontend are both skipped" +fi + +if [[ "${INCLUDE_INFRA}" -eq 0 ]]; then + echo "==> Done." + exit 0 +fi + +infra_images=( + "${SMARTFLOW_MYSQL_IMAGE:-mysql:8.0}" + "${SMARTFLOW_REDIS_IMAGE:-redis:7}" + "${SMARTFLOW_KAFKA_IMAGE:-apache/kafka:3.7.2}" + "${SMARTFLOW_ETCD_IMAGE:-quay.io/coreos/etcd:v3.5.5}" + "${SMARTFLOW_MINIO_IMAGE:-minio/minio:RELEASE.2023-03-20T20-16-18Z}" + "${SMARTFLOW_MILVUS_IMAGE:-milvusdb/milvus:v2.4.4}" + "${SMARTFLOW_ATTU_IMAGE:-zilliz/attu:v2.4.3}" +) + +for image_ref in "${infra_images[@]}"; do + if docker image inspect "${image_ref}" >/dev/null 2>&1; then + echo "==> Reuse local infra image ${image_ref}" + continue + fi + + echo "==> Pull infra image ${image_ref}" + docker pull "${image_ref}" +done + +rm -f "${infra_bundle_path}" +echo "==> Export infra bundle to ${infra_bundle_path}" +docker save -o "${infra_bundle_path}" "${infra_images[@]}" + +echo "==> Done." diff --git a/deploy/impact-rules.sh b/deploy/impact-rules.sh new file mode 100755 index 0000000..cb0602b --- /dev/null +++ b/deploy/impact-rules.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +set -euo pipefail + +base_ref="${1:-}" +head_ref="${2:-HEAD}" +output_file="${3:-}" + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${repo_root}" + +backend_services=(userauth notification active-scheduler schedule task task-class course memory agent taskclassforum tokenstore llm api) +selected_services=() +frontend_changed=0 +full_backend=0 + +add_service() { + local service="$1" + for current in "${selected_services[@]}"; do + if [[ "${current}" == "${service}" ]]; then + return 0 + fi + done + selected_services+=("${service}") +} + +have_ref() { + local ref="$1" + [[ -n "${ref}" ]] && git rev-parse --verify --quiet "${ref}^{commit}" >/dev/null +} + +if ! have_ref "${head_ref}"; then + echo "head ref not found: ${head_ref}" >&2 + exit 65 +fi + +app_tag="$(git rev-parse --short=12 "${head_ref}")" + +declare -a changed_files=() +if have_ref "${base_ref}"; then + mapfile -t changed_files < <(git diff --name-only "${base_ref}" "${head_ref}") +else + frontend_changed=1 + full_backend=1 +fi + +for file in "${changed_files[@]}"; do + case "${file}" in + README.md|docs/*) + ;; + frontend/*|deploy/nginx/*) + frontend_changed=1 + ;; + deploy/docker-pack.*|deploy/docker-load.sh|deploy/stage-release.sh|deploy/project-release.sh|deploy/project-rollback.sh|deploy/impact-rules.sh) + frontend_changed=1 + full_backend=1 + ;; + docker-compose.full.yml|frontend/nginx.conf|.env.full.example) + frontend_changed=1 + full_backend=1 + ;; + backend/Dockerfile|backend/config.docker.yaml|backend/shared/*|backend/client/*) + full_backend=1 + ;; + backend/gateway/*|backend/cmd/api/*) + add_service "api" + ;; + backend/cmd/userauth/*|backend/services/userauth/*) + add_service "userauth" + ;; + backend/cmd/notification/*|backend/services/notification/*) + add_service "notification" + ;; + backend/cmd/active-scheduler/*|backend/services/active_scheduler/*) + add_service "active-scheduler" + ;; + backend/cmd/schedule/*|backend/services/schedule/*) + add_service "schedule" + ;; + backend/cmd/task/*|backend/services/task/*) + add_service "task" + ;; + backend/cmd/task-class/*|backend/services/task_class/*) + add_service "task-class" + ;; + backend/cmd/course/*|backend/services/course/*) + add_service "course" + ;; + backend/cmd/memory/*|backend/services/memory/*) + add_service "memory" + ;; + backend/cmd/agent/*|backend/services/agent/*) + add_service "agent" + ;; + backend/cmd/taskclassforum/*|backend/services/taskclassforum/*) + add_service "taskclassforum" + ;; + backend/cmd/tokenstore/*|backend/services/tokenstore/*) + add_service "tokenstore" + ;; + backend/cmd/llm/*|backend/services/llm/*) + add_service "llm" + ;; + backend/*) + full_backend=1 + ;; + esac +done + +if [[ "${full_backend}" -eq 1 ]]; then + selected_services=("${backend_services[@]}") +fi + +if [[ "${frontend_changed}" -eq 1 ]]; then + add_service "frontend" +fi + +build_backend=0 +build_frontend=0 +for service in "${selected_services[@]}"; do + if [[ "${service}" == "frontend" ]]; then + build_frontend=1 + else + build_backend=1 + fi +done + +noop=0 +if [[ "${#selected_services[@]}" -eq 0 ]]; then + noop=1 +fi + +restart_csv="" +if [[ "${#selected_services[@]}" -gt 0 ]]; then + restart_csv="$(IFS=,; echo "${selected_services[*]}")" +fi + +content=$( + cat < "${output_file}" +else + printf '%s\n' "${content}" +fi diff --git a/deploy/nginx/default.conf b/deploy/nginx/default.conf new file mode 100644 index 0000000..d888d5e --- /dev/null +++ b/deploy/nginx/default.conf @@ -0,0 +1,86 @@ +server { + listen 80; + server_name smartmate.lecspace.com _; + return 301 https://$host$request_uri; +} + +server { + listen 80; + server_name git.lecspace.com; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + http2 on; + server_name smartmate.lecspace.com; + resolver 127.0.0.11 ipv6=off valid=30s; + + ssl_certificate /etc/nginx/certs/tls.crt; + ssl_certificate_key /etc/nginx/certs/tls.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:10m; + ssl_session_tickets off; + + root /usr/share/nginx/html; + index index.html; + + # 1. 生产环境统一由前端容器反代后端 API,前端继续使用相对路径 /api/v1。 + # 2. 关闭代理缓冲,避免 Agent SSE 响应被 Nginx 缓存后前端长时间收不到数据。 + location /api/ { + set $api_upstream http://api:8080; + proxy_pass $api_upstream; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Connection ""; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + add_header X-Accel-Buffering no; + } + + # 1. Vue Router 走 history 模式时,静态资源未命中需要回落到 index.html。 + # 2. 这里只负责前端页面回落,接口流量统一由上面的 /api 路由处理。 + location / { + try_files $uri $uri/ /index.html; + } +} + +server { + listen 443 ssl; + http2 on; + server_name git.lecspace.com; + resolver 127.0.0.11 ipv6=off valid=30s; + + ssl_certificate /etc/nginx/certs/git.lecspace.com_bundle.pem; + ssl_certificate_key /etc/nginx/certs/git.lecspace.com.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:10m; + ssl_session_tickets off; + + client_max_body_size 2g; + proxy_read_timeout 600s; + proxy_send_timeout 600s; + proxy_request_buffering off; + proxy_buffering off; + + # 1. Gitea Web、Container Registry 与 API 统一复用同一域名入口。 + # 2. 这里改成变量代理,避免 Nginx 启动时强制解析上游而影响离线语法校验。 + location / { + set $gitea_upstream http://gitea-web:3000; + proxy_pass $gitea_upstream; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port 443; + proxy_set_header Connection ""; + } +} diff --git a/deploy/project-release.sh b/deploy/project-release.sh new file mode 100755 index 0000000..26fdd64 --- /dev/null +++ b/deploy/project-release.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +set -euo pipefail + +# 1. 只负责把当前 release 目录里的编排资产和镜像包应用到运行目录。 +# 2. 不负责生成镜像,也不负责计算影响范围;这些工作由构建机和 impact-rules.sh 提前完成。 +# 3. 如果 release-plan.env 缺失或格式错误,直接失败,避免在生产机上猜测要重启哪些服务。 + +runtime_dir="${SMARTFLOW_RUNTIME_DIR:-/root/smartflow}" +release_dir="${SMARTFLOW_RELEASE_DIR:?SMARTFLOW_RELEASE_DIR is required}" +release_id="${SMARTFLOW_RELEASE_ID:?SMARTFLOW_RELEASE_ID is required}" +plan_file="${release_dir}/deploy/release-plan.env" +runtime_env="${runtime_dir}/.env" + +if [[ ! -f "${plan_file}" ]]; then + echo "release plan not found: ${plan_file}" >&2 + exit 66 +fi + +if [[ ! -f "${runtime_env}" ]]; then + echo "runtime env not found: ${runtime_env}" >&2 + exit 67 +fi + +source "${plan_file}" + +if [[ "${SMARTFLOW_NOOP:-0}" == "1" ]]; then + echo "release ${release_id} resolved to no-op" + exit 0 +fi + +set_env_var() { + local key="$1" + local value="$2" + local file="$3" + local tmp + + tmp="$(mktemp)" + awk -v key="${key}" -v value="${value}" ' + BEGIN { updated = 0 } + $0 ~ ("^" key "=") { + print key "=" value + updated = 1 + next + } + { print } + END { + if (updated == 0) { + print key "=" value + } + } + ' "${file}" > "${tmp}" + mv "${tmp}" "${file}" +} + +mkdir -p "${runtime_dir}/deploy/nginx" + +install -m 0644 "${release_dir}/docker-compose.full.yml" "${runtime_dir}/docker-compose.full.yml" +install -m 0644 "${release_dir}/deploy/nginx/default.conf" "${runtime_dir}/deploy/nginx/default.conf" + +if compgen -G "${release_dir}/.docker-bundles/*.tar" >/dev/null 2>&1; then + bash "${release_dir}/deploy/docker-load.sh" "${release_dir}/.docker-bundles" +fi + +if [[ "${SMARTFLOW_BUILD_BACKEND:-0}" == "1" ]]; then + set_env_var "SMARTFLOW_BACKEND_IMAGE" "${SMARTFLOW_BACKEND_IMAGE}" "${runtime_env}" +fi + +if [[ "${SMARTFLOW_BUILD_FRONTEND:-0}" == "1" ]]; then + set_env_var "SMARTFLOW_FRONTEND_IMAGE" "${SMARTFLOW_FRONTEND_IMAGE}" "${runtime_env}" +fi + +services=() +IFS=',' read -r -a raw_services <<< "${SMARTFLOW_RESTART_SERVICES:-}" +for service in "${raw_services[@]}"; do + if [[ -n "${service}" ]]; then + services+=("${service}") + fi +done + +if [[ "${#services[@]}" -eq 0 ]]; then + echo "release ${release_id} has no target services" + exit 0 +fi + +# 1. 使用 --no-deps 只重建命中的服务,避免后端小改动把整套依赖链一起拉起来。 +# 2. 如果后续服务新增或删减,只要 release-plan.env 给出的服务名同步更新,这里无需改脚本。 +# 3. 失败时直接退出,由上层薄封装决定是否切回旧 release。 +( + cd "${runtime_dir}" + docker compose -f docker-compose.full.yml up -d --no-deps "${services[@]}" +) + +if [[ "${SMARTFLOW_BUILD_BACKEND:-0}" == "1" || " ${services[*]} " == *" api "* ]]; then + curl -fsS http://127.0.0.1:8080/api/v1/health >/dev/null +fi + +if [[ " ${services[*]} " == *" frontend "* ]]; then + curl -k -fsS https://smartmate.lecspace.com/ >/dev/null + curl -k -fsS https://git.lecspace.com/ >/dev/null +fi + +printf '%s\n' "${release_id}" > "${SMARTFLOW_RELEASE_ROOT}/last_successful_release" +ln -sfn "${release_dir}" "${SMARTFLOW_CURRENT_LINK}" +echo "release ${release_id} deployed successfully" diff --git a/deploy/project-rollback.sh b/deploy/project-rollback.sh new file mode 100755 index 0000000..d0a4c39 --- /dev/null +++ b/deploy/project-rollback.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +# 1. 回滚本质上就是把目标旧 release 再部署一遍。 +# 2. 这里不单独复制逻辑,避免发布和回滚两套脚本漂移。 +# 3. 薄封装脚本传入哪个 release_id,这里就把哪个 release 当成目标版本重放。 + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "${script_dir}/project-release.sh" "$@" diff --git a/deploy/stage-release.sh b/deploy/stage-release.sh new file mode 100755 index 0000000..6977736 --- /dev/null +++ b/deploy/stage-release.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +release_dir="" +plan_file="" +bundle_dir=".docker-bundles" + +while [[ $# -gt 0 ]]; do + case "$1" in + --release-dir) + release_dir="$2" + shift 2 + ;; + --plan-file) + plan_file="$2" + shift 2 + ;; + --bundle-dir) + bundle_dir="$2" + shift 2 + ;; + *) + echo "unknown argument: $1" >&2 + exit 64 + ;; + esac +done + +if [[ -z "${release_dir}" || -z "${plan_file}" ]]; then + echo "usage: stage-release.sh --release-dir --plan-file [--bundle-dir ]" >&2 + exit 64 +fi + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +release_abs="${repo_root}/${release_dir}" +bundle_abs="${repo_root}/${bundle_dir}" + +rm -rf "${release_abs}" +mkdir -p "${release_abs}/deploy/nginx" "${release_abs}/deploy/certs" "${release_abs}/.docker-bundles" + +cp "${repo_root}/docker-compose.full.yml" "${release_abs}/docker-compose.full.yml" +cp "${repo_root}/deploy/docker-load.sh" "${release_abs}/deploy/docker-load.sh" +cp "${repo_root}/deploy/project-release.sh" "${release_abs}/deploy/project-release.sh" +cp "${repo_root}/deploy/project-rollback.sh" "${release_abs}/deploy/project-rollback.sh" +cp "${repo_root}/deploy/impact-rules.sh" "${release_abs}/deploy/impact-rules.sh" +cp "${repo_root}/deploy/nginx/default.conf" "${release_abs}/deploy/nginx/default.conf" +cp "${repo_root}/deploy/certs/README.md" "${release_abs}/deploy/certs/README.md" +cp "${plan_file}" "${release_abs}/deploy/release-plan.env" + +if compgen -G "${bundle_abs}/*.tar" >/dev/null 2>&1; then + cp "${bundle_abs}"/*.tar "${release_abs}/.docker-bundles/" +fi diff --git a/docker-compose.full.yml b/docker-compose.full.yml index 99d0055..7209685 100644 --- a/docker-compose.full.yml +++ b/docker-compose.full.yml @@ -293,6 +293,7 @@ services: - "${SMARTFLOW_FRONTEND_HTTPS_PORT:-443}:443" volumes: - ./deploy/certs:/etc/nginx/certs:ro + - ./deploy/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro depends_on: api: condition: service_started diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 799a5d2..d888d5e 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -4,6 +4,12 @@ server { return 301 https://$host$request_uri; } +server { + listen 80; + server_name git.lecspace.com; + return 301 https://$host$request_uri; +} + server { listen 443 ssl; http2 on; @@ -43,3 +49,38 @@ server { try_files $uri $uri/ /index.html; } } + +server { + listen 443 ssl; + http2 on; + server_name git.lecspace.com; + resolver 127.0.0.11 ipv6=off valid=30s; + + ssl_certificate /etc/nginx/certs/git.lecspace.com_bundle.pem; + ssl_certificate_key /etc/nginx/certs/git.lecspace.com.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:10m; + ssl_session_tickets off; + + client_max_body_size 2g; + proxy_read_timeout 600s; + proxy_send_timeout 600s; + proxy_request_buffering off; + proxy_buffering off; + + # 1. Gitea Web、Container Registry 与 API 统一复用同一域名入口。 + # 2. 这里改成变量代理,避免 Nginx 启动时强制解析上游而影响离线语法校验。 + location / { + set $gitea_upstream http://gitea-web:3000; + proxy_pass $gitea_upstream; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port 443; + proxy_set_header Connection ""; + } +}