chore: 重构 CI/CD 与 Docker 发布流程
Some checks failed
CI / ci (push) Failing after 2s

将部署链路调整为 CI 构建推送镜像、服务器拉取镜像运行,并拆分/复用 Gitea workflow 与公共准备脚本;同时统一 APP_NAME 与端口变量配置,补充 Docker 与 ESLint 相关配置文件以提升可维护性。

Made-with: Cursor
This commit is contained in:
2026-04-28 01:44:37 +08:00
parent 132f51705e
commit 3076b7ec54
16 changed files with 751 additions and 275 deletions

View File

@@ -0,0 +1,116 @@
#!/usr/bin/env bash
set -euo pipefail
# Reusable bootstrap for Gitea workflows:
# 1) checkout target commit
# 2) ensure Node version (install from domestic mirror if missing/mismatch)
# 3) ensure Yarn is available (and optionally pin version)
# 通用准备脚本:用于 CI/Deploy 前的代码检出、Node 准备、Yarn 准备。
REPO_URL="${1:?repo url required}"
GIT_SHA="${2:?git sha required}"
# 优先使用精确版本 NODE_VERSION未指定时按 NODE_MAJOR 自动解析最新小版本。
NODE_MAJOR="${NODE_MAJOR:-22}"
NODE_VERSION="${NODE_VERSION:-}"
YARN_VERSION="${YARN_VERSION:-stable}"
# 默认使用国内镜像,减少下载失败概率。
NODEJS_MIRROR="${NODEJS_MIRROR:-https://npmmirror.com/mirrors/node}"
NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmmirror.com}"
# 安装到用户目录,避免依赖 /opt 写权限。
NODE_INSTALL_ROOT="${NODE_INSTALL_ROOT:-$HOME/.local/node-ci}"
resolve_node_version() {
# 指定了精确版本时,直接使用。
if [ -n "${NODE_VERSION}" ]; then
echo "${NODE_VERSION#v}"
return
fi
# 未指定精确版本时,从镜像索引中选定 NODE_MAJOR 对应的最新版本。
local resolved
resolved="$(curl -fsSL "${NODEJS_MIRROR}/index.tab" | awk -F'\t' -v major="${NODE_MAJOR}" '
NR > 1 && $1 ~ ("^v" major "\\.") { print substr($1, 2); exit }
')"
if [ -z "${resolved}" ]; then
echo "failed to resolve latest Node ${NODE_MAJOR}.x from ${NODEJS_MIRROR}"
exit 1
fi
echo "${resolved}"
}
detect_arch() {
# 仅支持常见 Linux 架构;其他架构显式失败,避免下载错误包。
case "$(uname -m)" in
x86_64) echo "x64" ;;
aarch64 | arm64) echo "arm64" ;;
*)
echo "unsupported architecture: $(uname -m)"
exit 1
;;
esac
}
install_node() {
local target_version="$1"
local arch name url tarball install_dir bindir
arch="$(detect_arch)"
name="node-v${target_version}-linux-${arch}"
url="${NODEJS_MIRROR}/v${target_version}/${name}.tar.xz"
tarball="/tmp/${name}.tar.xz"
install_dir="${NODE_INSTALL_ROOT}/${name}"
bindir="${install_dir}/bin"
if [ ! -x "${bindir}/node" ]; then
# 下载并解压 Node 二进制包到本地缓存目录。
mkdir -p "${NODE_INSTALL_ROOT}"
curl -fsSL "${url}" -o "${tarball}"
tar -xJf "${tarball}" -C "${NODE_INSTALL_ROOT}"
rm -f "${tarball}"
fi
export PATH="${bindir}:${PATH}"
}
ensure_node() {
local target_version node_version
target_version="$(resolve_node_version)"
# 版本完全一致则直接复用,避免重复下载。
if command -v node >/dev/null 2>&1; then
node_version="$(node -v | sed 's/^v//')"
if [ "${node_version}" = "${target_version}" ]; then
return
fi
fi
install_node "${target_version}"
node_version="$(node -v | sed 's/^v//')"
if [ "${node_version}" != "${target_version}" ]; then
echo "failed to switch to Node ${target_version}, current=${node_version}"
exit 1
fi
}
# 首次执行时检出目标提交;已存在仓库则更新并强制切到指定 SHA。
if [ ! -d .git ]; then
git clone "${REPO_URL}" .
fi
git fetch --all --tags --prune
git checkout -f "${GIT_SHA}"
ensure_node
# Use domestic npm mirror for package-manager metadata/download.
npm config set registry "${NPM_REGISTRY}" >/dev/null 2>&1 || true
if ! command -v yarn >/dev/null 2>&1; then
# 通过 corepack 激活指定 Yarn 版本。
corepack enable
export COREPACK_NPM_REGISTRY="${NPM_REGISTRY}"
corepack prepare "yarn@${YARN_VERSION}" --activate
fi
echo "Node: $(node -v)"
echo "Yarn: $(yarn -v)"

36
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,36 @@
name: CI
on:
push:
branches: ["main"]
pull_request:
jobs:
# workflow_call 调用示例(先保留注释,不启用):
# ci:
# uses: ./.gitea/workflows/reusable-ci.yml
# with:
# node_major: "22"
# yarn_version: "stable"
# run_lint: true
# run_tsc: true
ci:
runs-on: ubuntu-latest
env:
NODE_MAJOR: "22"
YARN_VERSION: "stable"
steps:
- name: Prepare workspace (checkout + node/yarn)
run: |
set -euo pipefail
chmod +x .gitea/scripts/prepare-workspace.sh
./.gitea/scripts/prepare-workspace.sh \
"${{ github.server_url }}/${{ github.repository }}.git" \
"${{ github.sha }}"
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Lint
run: yarn lint
- name: TypeScript check
run: yarn tsc --noEmit

View File

@@ -0,0 +1,80 @@
name: Deploy Production
on:
push:
tags: ["v*"]
workflow_dispatch:
jobs:
deploy:
if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') }}
runs-on: ubuntu-latest
env:
NODE_MAJOR: "22"
YARN_VERSION: "stable"
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
DEPLOY_PORT: ${{ vars.DEPLOY_PORT }}
DEPLOY_USER: ${{ vars.DEPLOY_USER }}
DEPLOY_PATH: ${{ vars.DEPLOY_PATH }}
IMAGE_REPO: ${{ vars.IMAGE_REPO }}
REGISTRY: ${{ vars.REGISTRY }}
steps:
- name: Prepare workspace (checkout + node/yarn)
run: |
set -euo pipefail
chmod +x .gitea/scripts/prepare-workspace.sh
./.gitea/scripts/prepare-workspace.sh \
"${{ github.server_url }}/${{ github.repository }}.git" \
"${{ github.sha }}"
- name: Setup SSH
run: |
set -euo pipefail
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -p "${DEPLOY_PORT:-22}" "${DEPLOY_HOST}" >> ~/.ssh/known_hosts
- name: Prepare image tag
run: |
set -euo pipefail
if [[ "${GITHUB_REF:-}" == refs/tags/* ]]; then
IMAGE_TAG="${GITHUB_REF_NAME}"
else
IMAGE_TAG="${GITHUB_SHA:0:12}"
fi
echo "IMAGE_TAG=${IMAGE_TAG}" >> "$GITHUB_ENV"
- name: Build and push image
run: |
set -euo pipefail
if [ -z "${REGISTRY}" ] || [ -z "${IMAGE_REPO}" ]; then
echo "REGISTRY and IMAGE_REPO vars are required"
exit 1
fi
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${REGISTRY}" -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
docker build -f deploy/docker/Dockerfile -t "${IMAGE_REPO}:${IMAGE_TAG}" .
docker push "${IMAGE_REPO}:${IMAGE_TAG}"
- name: Deploy
run: |
set -euo pipefail
ssh -p "${DEPLOY_PORT:-22}" "${DEPLOY_USER}@${DEPLOY_HOST}" <<EOF
set -euo pipefail
cd "${DEPLOY_PATH}"
mkdir -p deploy/docker
cat > deploy/docker/.env <<EOT
${{ vars.DEPLOY_DOCKER_ENV }}
IMAGE_REPO=${IMAGE_REPO}
IMAGE_TAG=${IMAGE_TAG}
EOT
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${REGISTRY}" -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
docker compose -f deploy/docker/docker-compose.yml --env-file deploy/docker/.env pull app
docker compose -f deploy/docker/docker-compose.yml --env-file deploy/docker/.env up -d app
docker compose -f deploy/docker/docker-compose.yml ps
curl -fsS "http://127.0.0.1:\${HOST_BIND_PORT:-3000}/api/docs" >/dev/null
EOF

View File

@@ -0,0 +1,51 @@
name: Reusable CI
on:
workflow_call:
inputs:
node_major:
description: "Node 主版本(例如 22"
required: false
type: string
default: "22"
yarn_version:
description: "Yarn 版本(例如 stable 或 1.22.22"
required: false
type: string
default: "stable"
run_lint:
description: "是否执行 yarn lint"
required: false
type: boolean
default: true
run_tsc:
description: "是否执行 yarn tsc --noEmit"
required: false
type: boolean
default: true
jobs:
ci:
runs-on: ubuntu-latest
env:
NODE_MAJOR: ${{ inputs.node_major }}
YARN_VERSION: ${{ inputs.yarn_version }}
steps:
- name: Prepare workspace (checkout + node/yarn)
run: |
set -euo pipefail
chmod +x .gitea/scripts/prepare-workspace.sh
./.gitea/scripts/prepare-workspace.sh \
"${{ github.server_url }}/${{ github.repository }}.git" \
"${{ github.sha }}"
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Lint
if: ${{ inputs.run_lint }}
run: yarn lint
- name: TypeScript check
if: ${{ inputs.run_tsc }}
run: yarn tsc --noEmit