diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..1fc0586 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,130 @@ +name: Deploy + +on: + pull_request: + types: + - closed + branches: + - master + - main + workflow_dispatch: + inputs: + tag: + description: 'Image tag to deploy' + required: false + default: 'latest' + environment: + description: 'Deployment environment' + required: true + default: 'production' + type: choice + options: + - production + - staging + +env: + REGISTRY: code.littlelan.cn + IMAGE_NAME: carrot_bbs/backend + SERVER_HOST: 111.170.19.33 + SERVER_USER: root + CONTAINER_NAME: carrot_bbs + CONTAINER_NETWORK: 1panel-network + +jobs: + deploy: + runs-on: ubuntu-latest + if: ${{ (github.event.pull_request.merged == true) || (github.event_name == 'workflow_dispatch') }} + environment: production + steps: + - name: Deploy via SSH + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ env.SERVER_HOST }} + username: ${{ env.SERVER_USER }} + password: ${{ secrets.SERVER_PASSWORD }} + script: | + set -e + + IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.tag || 'latest' }}" + CONTAINER="${{ env.CONTAINER_NAME }}" + NETWORK="${{ env.CONTAINER_NETWORK }}" + + echo "=== Pulling image: ${IMAGE} ===" + docker pull ${IMAGE} + + echo "=== Stopping old container ===" + docker stop ${CONTAINER} 2>/dev/null || true + docker rm ${CONTAINER} 2>/dev/null || true + + echo "=== Starting new container ===" + docker run -d \ + --name ${CONTAINER} \ + --network ${NETWORK} \ + --restart unless-stopped \ + -p 8080:8080 \ + -p 50051:50051 \ + -e APP_DATABASE_TYPE=postgres \ + -e APP_DATABASE_POSTGRES_HOST=1Panel-postgresql-t0g7 \ + -e APP_DATABASE_POSTGRES_PORT=5432 \ + -e APP_DATABASE_POSTGRES_USER=carrot_bbs \ + -e "APP_DATABASE_POSTGRES_PASSWORD=${{ secrets.DB_PASSWORD }}" \ + -e APP_DATABASE_POSTGRES_DBNAME=carrot_bbs \ + -e APP_REDIS_TYPE=redis \ + -e APP_REDIS_REDIS_HOST=1Panel-redis-dfmM \ + -e APP_REDIS_REDIS_PORT=6379 \ + -e "APP_REDIS_REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}" \ + -e APP_REDIS_REDIS_DB=0 \ + -e APP_S3_ENDPOINT=files.littlelan.cn \ + -e "APP_S3_ACCESS_KEY=${{ secrets.S3_ACCESS_KEY }}" \ + -e "APP_S3_SECRET_KEY=${{ secrets.S3_SECRET_KEY }}" \ + -e APP_S3_BUCKET=test \ + -e APP_S3_DOMAIN=files.littlelan.cn \ + -e APP_S3_USE_SSL=true \ + -e "APP_JWT_SECRET=${{ secrets.JWT_SECRET }}" \ + -e APP_ENCRYPTION_ENABLED=true \ + -e "APP_ENCRYPTION_KEY=${{ secrets.ENCRYPTION_KEY }}" \ + -e APP_ENCRYPTION_KEY_VERSION=1 \ + -e APP_GORSE_ENABLED=true \ + -e APP_GORSE_ADDRESS=http://111.170.19.33:8088 \ + -e "APP_GORSE_IMPORT_PASSWORD=${{ secrets.GORSE_IMPORT_PASSWORD }}" \ + -e "APP_GORSE_EMBEDDING_API_KEY=${{ secrets.GORSE_EMBEDDING_API_KEY }}" \ + -e APP_GORSE_EMBEDDING_URL=https://api.littlelan.cn/v1/embeddings \ + -e APP_GORSE_EMBEDDING_MODEL=BAAI/bge-m3 \ + -e APP_OPENAI_ENABLED=true \ + -e APP_OPENAI_BASE_URL=https://api.littlelan.cn/ \ + -e "APP_OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" \ + -e APP_OPENAI_MODERATION_MODEL=qwen3.5-plus \ + -e APP_OPENAI_MODERATION_MAX_IMAGES_PER_REQUEST=3 \ + -e APP_OPENAI_REQUEST_TIMEOUT=30 \ + -e APP_OPENAI_STRICT_MODERATION=false \ + -e APP_EMAIL_ENABLED=true \ + -e APP_EMAIL_HOST=smtp.exmail.qq.com \ + -e APP_EMAIL_PORT=465 \ + -e APP_EMAIL_USERNAME=no-reply@qczlit.cn \ + -e "APP_EMAIL_PASSWORD=${{ secrets.EMAIL_PASSWORD }}" \ + -e APP_EMAIL_FROM_ADDRESS=no-reply@qczlit.cn \ + -e APP_EMAIL_FROM_NAME=Carrot_BBS \ + -e APP_EMAIL_USE_TLS=true \ + -e APP_EMAIL_TIMEOUT=15 \ + -e APP_GRPC_ENABLED=true \ + -e APP_GRPC_PORT=50051 \ + -e APP_SERVER_MODE=release \ + ${IMAGE} + + echo "=== Waiting for startup ===" + sleep 5 + + echo "=== Health check ===" + for i in 1 2 3; do + if curl -sf http://localhost:8080/health; then + echo "Health check passed!" + break + fi + echo "Attempt $i failed, retrying..." + sleep 3 + done + + echo "=== Container logs ===" + docker logs ${CONTAINER} --tail 20 + + echo "=== Deployment completed ===" diff --git a/.gitignore b/.gitignore index 3617ddb..7c9be17 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ logs/ .env.* -plans/ \ No newline at end of file +plans/ +docs/ \ No newline at end of file diff --git a/Dockerfile.new b/Dockerfile.new new file mode 100644 index 0000000..0e45ba7 --- /dev/null +++ b/Dockerfile.new @@ -0,0 +1,52 @@ +# Build stage +FROM golang:1.22-alpine AS builder + +WORKDIR /build + +# Install dependencies +RUN apk add --no-cache git ca-certificates tzdata + +# Copy go mod files +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build binary +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o server ./cmd/server + +# Runtime stage +FROM debian:bookworm-slim + +# Install runtime dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + tzdata \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean + +# Create non-root user +RUN groupadd -r app && useradd -r -g app app + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /build/server ./carrot_bbs +COPY --from=builder /build/configs ./configs + +# Set ownership +RUN chown -R app:app /app + +# Switch to non-root user +USER app + +# Expose ports +EXPOSE 8080 50051 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/health || exit 1 + +# Run the binary +CMD ["./carrot_bbs"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..face563 --- /dev/null +++ b/Makefile @@ -0,0 +1,59 @@ +.PHONY: build test run docker docker-push clean help + +APP_NAME := carrot_bbs +VERSION := $(shell git rev-parse --short HEAD 2>/dev/null || echo "dev") +BUILD_TIME := $(shell date -u '+%Y-%m-%d_%H:%M:%S') +LDFLAGS := -ldflags "-s -w -X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME)" + +REGISTRY := code.littlelan.cn +IMAGE_NAME := carrot_bbs/backend + +help: + @echo "Available targets:" + @echo " make build - Build the binary" + @echo " make test - Run tests" + @echo " make run - Run locally" + @echo " make docker - Build Docker image" + @echo " make docker-push - Push Docker image to registry" + @echo " make clean - Clean build artifacts" + +build: + @echo "Building $(APP_NAME)..." + CGO_ENABLED=0 go build $(LDFLAGS) -o server ./cmd/server + +test: + @echo "Running tests..." + go test -v -race -coverprofile=coverage.out ./... + go tool cover -html=coverage.out -o coverage.html + +run: + @echo "Running $(APP_NAME)..." + go run ./cmd/server + +docker: + @echo "Building Docker image..." + docker build -t $(REGISTRY)/$(IMAGE_NAME):$(VERSION) -t $(REGISTRY)/$(IMAGE_NAME):latest . + +docker-push: + @echo "Pushing Docker image..." + docker push $(REGISTRY)/$(IMAGE_NAME):$(VERSION) + docker push $(REGISTRY)/$(IMAGE_NAME):latest + +clean: + @echo "Cleaning..." + rm -f server + rm -f coverage.out coverage.html + go clean + +lint: + @echo "Running linter..." + golangci-lint run ./... + +fmt: + @echo "Formatting code..." + go fmt ./... + goimports -w . + +wire: + @echo "Generating wire..." + cd cmd/server && wire