feat:增加cicd
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# ===== 版本控制 =====
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# ===== 文档(不参与编译)=====
|
||||
*.md
|
||||
docs/
|
||||
design/
|
||||
|
||||
# ===== 本地运行产物(日志)=====
|
||||
storage/
|
||||
|
||||
# ===== 配置与密钥:配置在运行时挂载,禁止进入构建上下文与镜像层 =====
|
||||
configs/
|
||||
|
||||
# ===== 其他与编译无关资产 =====
|
||||
scripts/
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
@@ -0,0 +1,123 @@
|
||||
name: 构建并部署
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY: git.lzcwy.cn
|
||||
IMAGE: git.lzcwy.cn/zhengchiliu/zt-closet-manage-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_MANAGE_REGISTRY_USER }}
|
||||
REGISTRY_TOKEN: ${{ secrets.ZT_CLOSET_MANAGE_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_MANAGE_PROD_SSH_KEY }}
|
||||
SSH_HOST: ${{ vars.ZT_CLOSET_MANAGE_PROD_SSH_HOST }}
|
||||
SSH_USER: ${{ vars.ZT_CLOSET_MANAGE_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-manage-backend
|
||||
IMAGE=git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend
|
||||
DIR=/opt/zt-closet-manage
|
||||
|
||||
mkdir -p "$DIR/configs" "$DIR/storage/logs"
|
||||
# 容器内进程是 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 \
|
||||
-e TZ=Asia/Shanghai \
|
||||
--log-opt max-size=50m \
|
||||
--log-opt max-file=5 \
|
||||
$IMAGE:latest
|
||||
|
||||
sleep 5
|
||||
# 健康检查端口须与生产 configs/config.yaml 的 Server.HttpPort 一致(当前 8081)
|
||||
if (command -v curl >/dev/null && curl -fsS http://127.0.0.1:8081/ping) \
|
||||
|| (command -v wget >/dev/null && wget -qO- http://127.0.0.1:8081/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 \
|
||||
-e TZ=Asia/Shanghai \
|
||||
--log-opt max-size=50m \
|
||||
--log-opt max-file=5 \
|
||||
"$OLD_IMAGE"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
REMOTE
|
||||
|
||||
- name: 部署结果
|
||||
if: always()
|
||||
run: echo "镜像 ${{ env.IMAGE }}:${{ steps.meta.outputs.tag }} 部署流程结束"
|
||||
@@ -86,7 +86,7 @@ go run .
|
||||
验证健康检查:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/ping
|
||||
curl http://localhost:8081/ping
|
||||
# {"message":"pong"}
|
||||
```
|
||||
|
||||
@@ -231,6 +231,38 @@ curl http://localhost:8080/ping
|
||||
- 登录接口对「账号不存在」与「密码错误」返回同一错误码,避免账号枚举攻击。
|
||||
- 生产环境请将 `Server.RunMode` 设为 `release`,并通过反向代理对外提供 HTTPS。
|
||||
|
||||
## CI/CD 与部署
|
||||
|
||||
### 流水线(Gitea Actions)
|
||||
|
||||
`.gitea/workflows/deploy.yaml` 提供「构建并部署」流水线:
|
||||
|
||||
| 触发方式 | 说明 |
|
||||
| --- | --- |
|
||||
| 推送 `v*` 标签 | 如 `git tag v1.0.0 && git push origin v1.0.0` |
|
||||
| 手动触发 | Gitea 仓库页面 Actions 标签页 → 选择「构建并部署」→ Run workflow |
|
||||
|
||||
流程:拉取代码 → 构建镜像(多阶段 Dockerfile,静态编译)→ 推送 `git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend`(tag + latest)→ SSH 部署生产机 → `/ping` 健康检查;**检查失败自动输出日志并回滚到旧镜像**。
|
||||
|
||||
> 管理后台无数据库迁移命令(`admin_users` 表由服务启动时 GORM AutoMigrate 幂等创建),因此没有业务服务那样的迁移流水线。
|
||||
|
||||
### Gitea 仓库 Secrets / Variables(需自行创建)
|
||||
|
||||
| 类型 | 名称 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| Secret | `ZT_CLOSET_MANAGE_REGISTRY_USER` | Gitea 镜像仓库登录用户名 |
|
||||
| Secret | `ZT_CLOSET_MANAGE_REGISTRY_TOKEN` | Gitea 镜像仓库登录令牌 |
|
||||
| Secret | `ZT_CLOSET_MANAGE_PROD_SSH_KEY` | 生产机部署 SSH 私钥 |
|
||||
| Variable | `ZT_CLOSET_MANAGE_PROD_SSH_HOST` | 生产机 IP/域名 |
|
||||
| Variable | `ZT_CLOSET_MANAGE_PROD_SSH_USER` | 生产机 SSH 用户 |
|
||||
|
||||
### 生产机准备
|
||||
|
||||
- 部署目录:`/opt/zt-closet-manage`(流水线会自动创建 `configs/` 与 `storage/logs/`)。
|
||||
- 手动准备 `configs/config.yaml`(见上文配置说明);其 `Server.HttpPort` 必须为 `8081`(与流水线健康检查端口一致)。
|
||||
- 容器以非 root 用户(uid 10001)运行,日志目录权限由流水线自动设置。
|
||||
- 本地构建验证:`docker build --build-arg GOPROXY=https://goproxy.cn,direct -f scripts/dockerfile/Dockerfile -t zt-closet-manage-backend:test .`
|
||||
|
||||
## 部署提示
|
||||
|
||||
- **工作目录**:启动时必须位于项目根目录(配置路径固定为 `configs/config.yaml`)。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Server:
|
||||
Domain: admin.example.com #服务域名,用于 CORS 允许来源(http/https 均放行);置 * 或不填则允许所有来源
|
||||
RunMode: release #gin运行模式,debug 和 release
|
||||
HttpPort: 8080
|
||||
HttpPort: 8081
|
||||
ReadTimeout: 60
|
||||
WriteTimeout: 60
|
||||
Log:
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
ARG GO_VERSION=1.26
|
||||
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-manage-backend .
|
||||
|
||||
# ============================== 运行阶段 ==============================
|
||||
FROM alpine:${ALPINE_VERSION}
|
||||
|
||||
# ca-certificates:访问 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-manage-backend /app/zt-closet-manage-backend
|
||||
|
||||
# configs/ :配置文件挂载点(config.yaml 含密钥,不打入镜像,必须运行时挂载)
|
||||
# storage/logs:日志目录(对应配置 Log.LogSavePath)
|
||||
RUN mkdir -p /app/configs /app/storage/logs \
|
||||
&& chown -R app:app /app
|
||||
|
||||
ENV TZ=Asia/Shanghai
|
||||
# 仅用于 EXPOSE 与健康检查:真实监听端口由 configs/config.yaml 的 Server.HttpPort 决定,两者需保持一致(当前 8081)。
|
||||
ENV APP_PORT=8081
|
||||
|
||||
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
|
||||
|
||||
# 启动服务(admin_users 表由服务启动时 AutoMigrate 幂等创建,无独立迁移命令)
|
||||
CMD ["/app/zt-closet-manage-backend"]
|
||||
Reference in New Issue
Block a user