feat:增加流水线

This commit is contained in:
2026-09-11 22:26:10 +08:00
parent 9f22e6ccae
commit 780f547c02
5 changed files with 378 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# ===== 版本控制 =====
.git
.gitignore
# ===== 文档与设计稿(不参与编译)=====
*.md
docs/
design/
# ===== 本地运行产物(日志、上传临时文件)=====
storage/
# ===== 配置与密钥:配置在运行时挂载,禁止进入构建上下文与镜像层 =====
configs/
# ===== Python 推理服务与模型权重(独立部署,体积大)=====
thirdparty/
# ===== 其他与编译无关资产 =====
resources/
sql/
scripts/
Dockerfile
.dockerignore
+122
View File
@@ -0,0 +1,122 @@
name: 构建并部署
on:
push:
branches: [main]
tags: ['v*']
workflow_dispatch:
concurrency:
group: deploy
cancel-in-progress: false
env:
REGISTRY: git.lzcwy.cn
IMAGE: git.lzcwy.cn/zhengchiliu/zt-closet-backend
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: 拉取代码
uses: actions/checkout@v4
- name: 确认 docker 可用
run: docker version --format 'Server: {{.Server.Version}}'
- name: 计算镜像标签
id: meta
run: |
case "$GITEA_REF" in
refs/tags/*) TAG="${GITEA_REF#refs/tags/}" ;;
*) TAG="sha-$(echo "$GITEA_SHA" | cut -c1-7)" ;;
esac
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "镜像标签: $TAG"
- name: 登录 Gitea 镜像仓库
env:
REGISTRY_USER: ${{ secrets.ZT_CLOSET_REGISTRY_USER }}
REGISTRY_TOKEN: ${{ secrets.ZT_CLOSET_REGISTRY_TOKEN }}
run: |
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
- name: 构建并推送
run: |
# 生产机若是 ARM(如甲骨文 ARM 机),去掉下一行注释并改成 linux/arm64
# PLATFORM="--platform linux/arm64"
docker build \
--build-arg GOPROXY=https://goproxy.cn,direct \
-f scripts/dockerfile/Dockerfile \
-t "$IMAGE:${{ steps.meta.outputs.tag }}" \
-t "$IMAGE:latest" \
.
docker push "$IMAGE:${{ steps.meta.outputs.tag }}"
docker push "$IMAGE:latest"
- name: SSH 部署到生产机
env:
SSH_KEY: ${{ secrets.ZT_CLOSET_PROD_SSH_KEY }}
SSH_HOST: ${{ vars.ZT_CLOSET_PROD_SSH_HOST }}
SSH_USER: ${{ vars.ZT_CLOSET_PROD_SSH_USER }}
run: |
mkdir -p ~/.ssh && chmod 700 ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
SSHOPT="-i ~/.ssh/deploy_key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
ssh $SSHOPT "$SSH_USER@$SSH_HOST" 'bash -s' <<'REMOTE'
set -e
APP=zt-closet-backend
IMAGE=git.lzcwy.cn/zhengchiliu/zt-closet-backend
DIR=/opt/zt-closet
mkdir -p "$DIR/configs" "$DIR/storage/logs" "$DIR/storage/uploads"
# 容器内进程是 uid 10001,日志/上传目录要给它写权限
chown -R 10001:10001 "$DIR/storage"
# 记住旧镜像,用于失败回滚
OLD_IMAGE=$(docker inspect -f '{{.Image}}' $APP 2>/dev/null || echo "")
docker pull $IMAGE:latest
docker rm -f $APP 2>/dev/null || true
docker run -d \
--name $APP \
--network host \
--restart unless-stopped \
-v $DIR/configs:/app/configs:ro \
-v $DIR/storage/logs:/app/storage/logs \
-v $DIR/storage/uploads:/app/storage/uploads \
-e TZ=Asia/Shanghai \
--log-opt max-size=50m \
--log-opt max-file=5 \
$IMAGE:latest
sleep 5
if (command -v curl >/dev/null && curl -fsS http://127.0.0.1:8080/ping) \
|| (command -v wget >/dev/null && wget -qO- http://127.0.0.1:8080/ping); then
echo " 健康检查通过"
docker image prune -f
else
echo " 健康检查失败,回滚"
docker logs --tail 50 $APP
docker rm -f $APP
if [ -n "$OLD_IMAGE" ]; then
docker run -d \
--name $APP \
--network host \
--restart unless-stopped \
-v $DIR/configs:/app/configs:ro \
-v $DIR/storage/logs:/app/storage/logs \
-v $DIR/storage/uploads:/app/storage/uploads \
-e TZ=Asia/Shanghai \
"$OLD_IMAGE"
fi
exit 1
fi
REMOTE
- name: 部署结果
if: always()
run: echo "镜像 ${{ env.IMAGE }}:${{ steps.meta.outputs.tag }} 部署流程结束"
+82
View File
@@ -0,0 +1,82 @@
name: 数据库迁移 - 回滚(危险)
on:
workflow_dispatch:
inputs:
confirm:
description: '将删除所有迁移创建的表及其数据,不可恢复。确认请输入 DELETE(大写)'
required: true
default: ''
concurrency:
group: migrate
cancel-in-progress: false
env:
REGISTRY: git.lzcwy.cn
IMAGE: git.lzcwy.cn/zhengchiliu/zt-closet-backend
MIGRATE_TAG: migrate
jobs:
migrate-down:
runs-on: ubuntu-latest
steps:
- name: 校验确认词
env:
IN_CONFIRM: ${{ gitea.event.inputs.confirm }}
run: |
if [ "${IN_CONFIRM:-}" != "DELETE" ]; then
echo "确认词不对(收到 '${IN_CONFIRM:-空}')。必须输入 DELETE 才会执行。" >&2
exit 1
fi
echo "确认通过"
- name: 拉取代码
uses: actions/checkout@v4
- name: 确认 docker 可用
run: docker version --format 'Server: {{.Server.Version}}'
- name: 登录 Gitea 镜像仓库
env:
REGISTRY_USER: ${{ secrets.ZT_CLOSET_REGISTRY_USER }}
REGISTRY_TOKEN: ${{ secrets.ZT_CLOSET_REGISTRY_TOKEN }}
run: |
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
- name: 构建并推送迁移镜像
run: |
docker build \
-f scripts/dockerfile/Dockerfile.migrate \
-t "$IMAGE:$MIGRATE_TAG" \
.
docker push "$IMAGE:$MIGRATE_TAG"
- name: 生产机执行回滚(结束即删容器)
env:
SSH_KEY: ${{ secrets.ZT_CLOSET_PROD_SSH_KEY }}
SSH_HOST: ${{ vars.ZT_CLOSET_PROD_SSH_HOST }}
SSH_USER: ${{ vars.ZT_CLOSET_PROD_SSH_USER }}
run: |
mkdir -p ~/.ssh && chmod 700 ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
SSHOPT="-i ~/.ssh/deploy_key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
ssh $SSHOPT "$SSH_USER@$SSH_HOST" 'bash -s' <<'REMOTE'
set -e
DIR=/opt/zt-closet
IMAGE=git.lzcwy.cn/zhengchiliu/zt-closet-backend:migrate
cd "$DIR" || { echo "目录不存在: $DIR"; exit 1; }
[ -f configs/config.yaml ] || { echo "缺 configs/config.yaml"; exit 1; }
docker pull "$IMAGE"
docker run --rm \
--network host \
-v "$DIR/configs:/app/configs:ro" \
-v zt-go-build-cache:/root/.cache/go-build \
-e TZ=Asia/Shanghai \
"$IMAGE" down
REMOTE
+88
View File
@@ -0,0 +1,88 @@
name: 数据库迁移 - 升级
on:
workflow_dispatch:
inputs:
check_only:
description: '只查看当前版本和脏库状态(执行 version,不改动数据库)'
required: false
default: false
type: boolean
concurrency:
group: migrate
cancel-in-progress: false
env:
REGISTRY: git.lzcwy.cn
IMAGE: git.lzcwy.cn/zhengchiliu/zt-closet-backend
MIGRATE_TAG: migrate
jobs:
migrate-up:
runs-on: ubuntu-latest
steps:
- name: 拉取代码
uses: actions/checkout@v4
- name: 确认 docker 可用
run: docker version --format 'Server: {{.Server.Version}}'
- name: 决定动作
id: args
env:
IN_CHECK: ${{ gitea.event.inputs.check_only }}
run: |
if [ "${IN_CHECK:-false}" = "true" ]; then
echo "action=version" >> "$GITHUB_OUTPUT"
echo "动作: version(只读)"
else
echo "action=up" >> "$GITHUB_OUTPUT"
echo "动作: up"
fi
- name: 登录 Gitea 镜像仓库
env:
REGISTRY_USER: ${{ secrets.ZT_CLOSET_REGISTRY_USER }}
REGISTRY_TOKEN: ${{ secrets.ZT_CLOSET_REGISTRY_TOKEN }}
run: |
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
- name: 构建并推送迁移镜像
run: |
docker build \
-f scripts/dockerfile/Dockerfile.migrate \
-t "$IMAGE:$MIGRATE_TAG" \
.
docker push "$IMAGE:$MIGRATE_TAG"
- name: 生产机执行(结束即删容器)
env:
SSH_KEY: ${{ secrets.ZT_CLOSET_PROD_SSH_KEY }}
SSH_HOST: ${{ vars.ZT_CLOSET_PROD_SSH_HOST }}
SSH_USER: ${{ vars.ZT_CLOSET_PROD_SSH_USER }}
ACTION: ${{ steps.args.outputs.action }}
run: |
mkdir -p ~/.ssh && chmod 700 ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
SSHOPT="-i ~/.ssh/deploy_key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
ssh $SSHOPT "$SSH_USER@$SSH_HOST" "bash -s -- '$ACTION'" <<'REMOTE'
set -e
ACTION="$1"
DIR=/opt/zt-closet
IMAGE=git.lzcwy.cn/zhengchiliu/zt-closet-backend:migrate
cd "$DIR" || { echo "目录不存在: $DIR"; exit 1; }
[ -f configs/config.yaml ] || { echo "缺 configs/config.yaml"; exit 1; }
docker pull "$IMAGE"
docker run --rm \
--network host \
-v "$DIR/configs:/app/configs:ro" \
-v zt-go-build-cache:/root/.cache/go-build \
-e TZ=Asia/Shanghai \
"$IMAGE" "$ACTION"
REMOTE
+62
View File
@@ -0,0 +1,62 @@
ARG GO_VERSION=1.27
ARG ALPINE_VERSION=3.22
# ============================== 构建阶段 ==============================
FROM golang:${GO_VERSION}-alpine AS builder
# TARGETOS/TARGETARCH 由 BuildKit 在 --platform 下自动注入,此处默认值保证
# 未启用 buildx 时也能正常构建(默认 linux/amd64)。
ARG TARGETOS=linux
ARG TARGETARCH=amd64
# Go 模块代理:默认官方代理;国内或受限网络可覆盖:
# --build-arg GOPROXY=https://goproxy.cn,direct
ARG GOPROXY=https://proxy.golang.org,direct
ENV GOPROXY=${GOPROXY}
WORKDIR /src
# 先只复制依赖清单,源码未变动时复用 go mod 下载缓存,加快重建。
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# 静态编译:CGO_ENABLED=0 使二进制不依赖 libc,可运行在任意精简基础镜像;
# -trimpath 去除本地绝对路径保证构建可复现,-s -w 去除符号表与调试信息以减小体积。
ENV CGO_ENABLED=0
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w" -o /out/zt-closet-backend .
# ============================== 运行阶段 ==============================
FROM alpine:${ALPINE_VERSION}
# ca-certificates:访问 COS / 微信支付 / 大模型网关等 HTTPS 端点需要根证书;
# tzdata:业务按本地时区计算「每日 0 点」额度重置与运势生成,需与 TZ 配合使用;
# 运行进程使用非 root 用户 app(uid/gid 10001)。
RUN apk add --no-cache ca-certificates tzdata \
&& addgroup -g 10001 -S app \
&& adduser -u 10001 -S -G app app
WORKDIR /app
COPY --from=builder /out/zt-closet-backend /app/zt-closet-backend
# configs/ :配置文件挂载点(config.yaml 含密钥,不打入镜像,必须运行时挂载)
# storage/logs:日志目录(对应配置 Log.LogSavePath)
# storage/uploads:上传临时目录(对应配置 Upload.TmpSavePath)
RUN mkdir -p /app/configs /app/storage/logs /app/storage/uploads \
&& chown -R app:app /app
ENV TZ=Asia/Shanghai
# 仅用于 EXPOSE 与健康检查:真实监听端口由 configs/config.yaml 的 Server.HttpPort 决定,两者需保持一致。
ENV APP_PORT=8080
USER app
EXPOSE ${APP_PORT}
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -q -O - "http://127.0.0.1:${APP_PORT}/ping" || exit 1
# 启动服务(数据库迁移在容器外通过 go run ./cmd/migrate 手动执行)
CMD ["/app/zt-closet-backend"]