我有一个自建的Gitea跑在一个配置还不错的服务器上,在上次
看到联调时后端出各种不太聪明的Bug意识到单元测试的重要性、与服务器打交道多了后厌倦了手动部署···
于是我决定,尝试自动化测试和部署。
说是自动化测试和部署,主要还是解决自动化部署的问题。
Gitea的Action
Gitea可以在提交时、打标签时等时候,触发一个自动执行的程序,这个自动程序叫Action,Github也有,而且大同小异。
项目的 .gitea/workflows 下的所有yml文件会被扫描作为Action,Action执行的时机和内容都在文件里面写着。
开始之前
Gitea开箱即用,但Gitea的Action不是,自建Gitea需要事先部署Gitea Runner。
参考Gitea的文档。
Runner可以是可执行文件部署或者Docker部署,但无论哪种部署方式,机器上都必须有Docker。
无论Runner跑在哪里,Runner在执行任务的时候都会为任务临时启动一个Docker容器,且容器内能够操作Docker。
Action模板
部署Hugo静态网站
Action用的SH脚本:build.sh
这个Action会在v开头的标签推送到服务器时触发,自动准备Hugo环境、构建项目、向生产服务器上传构建的内容。
需要在Gitea的项目配置里面配置这些密钥:
PROD_PATH:生产服务器上传最终内容的目录
PROD_SERVER:SSH IP
PROD_USER: SSH 用户名
PROD_PASSWORD:SSH密码
name: 部署开发服务器
on:
push:
tags:
- "v*"
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: 签出代码
uses: actions/checkout@v3
- name: 构建项目
run: |
chmod +x ./build.sh
./build.sh # 使用密码认证的部署方式
- name: 部署到生产服务器
run: |
sudo apt-get update && sudo apt-get install -y sshpass openssh-client rsync
echo "准备部署到生产服务器..."
# 配置 SSH 以跳过主机密钥验证
mkdir -p ~/.ssh
echo "Host *" >> ~/.ssh/config
echo " StrictHostKeyChecking no" >> ~/.ssh/config
echo " UserKnownHostsFile=/dev/null" >> ~/.ssh/config
chmod 600 ~/.ssh/config
# 执行部署
sshpass -p "${{ secrets.PROD_PASSWORD }}" rsync -avz --delete -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" ./public/ ${{ secrets.PROD_USER }}@${{ secrets.PROD_SERVER }}:${{ secrets.PROD_PATH }}对于其他技术栈的纯前端项目,可以修改Action中的./public/ (构建结果目录)和构建项目时执行的./build.sh (例如换成npm run build,官方的ubuntu-latest上有npm)
部署开发用Golang后端
这个Action会在每次推送时触发,构建Docker并在Runner所在的机器上部署Docker。
name: 部署开发服务器
run-name: 正在部署开发服务器 🚀
on: [push]
branches:
- master
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: 📥 检出代码
uses: actions/checkout@v4
- name: 🐳 构建并部署
run: |
chmod +x scripts/deploy.sh
scripts/deploy.sh
deploy.sh:
#!/bin/bash
# Docker Compose 部署脚本配置
COMPOSE_FILE="docker-compose.yml"
SERVICE_NAME="my-server"
PORT="8080"
echo "🚀 开始使用 Docker Compose 部署 ${SERVICE_NAME}..."
# 检查 docker-compose.yml 文件是否存在
if [ ! -f "${COMPOSE_FILE}" ]; then
echo "❌ ${COMPOSE_FILE} 文件不存在"
exit 1
fi
# 停止并删除旧容器
echo "🗑️ 停止旧服务..."
docker compose down || true
# 强制重新构建镜像(清理构建缓存)
echo "🧹 清理旧镜像和构建缓存..."
docker compose down --rmi local || true
# docker builder prune -f || true
# 构建并启动服务
echo "📦 重新构建并启动服务..."
docker compose up -d --build --force-recreate || {
echo "❌ 服务启动失败"
exit 1
}
# 等待服务启动
echo "⏳ 等待服务启动..."
sleep 10
# 检查服务状态
for i in {1..10}; do
echo "🔍 第 $i 次检查服务状态..."
if [ "$(docker compose ps -q ${SERVICE_NAME})" ]; then
echo "✅ 服务已成功启动"
break
else
if [ $i -eq 10 ]; then
echo "❌ 服务启动失败"
docker compose logs ${SERVICE_NAME}
exit 1
else
echo "⏳ 服务尚未启动,稍后重试..."
sleep 5
fi
fi
done
echo "🎉 部署完成!"
echo "📊 部署信息:"
echo " 服务名称: ${SERVICE_NAME}"
echo " 服务端口: ${PORT}"
echo " 服务地址: http://localhost:${PORT}"
echo "📋 服务状态:"
docker compose ps
Dockerfile:
FROM golang:1.24-alpine AS builder
WORKDIR /app
RUN apk add --no-cache git ca-certificates tzdata make ffmpeg
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN make test
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags '-w -s' -o main cmd/api/main.go
FROM alpine:latest
RUN apk --no-cache add ca-certificates curl tzdata ffmpeg && \
addgroup -g 1000 appgroup && \
adduser -u 1000 -G appgroup -s /bin/sh -D appuser
WORKDIR /app
COPY --from=builder /app/main .
#COPY --from=builder /app/.env* ./
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
CMD ["./main"]
docker-compose.yml:
services:
my-server:
build: .
ports:
- "8080:8080"
environment: {}
env_file:
- .env
restart: unless-stopped
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:8080/health" ]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
- main
networks:
main:
driver: bridgedeploy.sh和docker-compose.yml中的名称 my-server 应该是相同的,用于脚本检测容器是否启动成功。