From c19b3b7b0522b29fc484412e483e1b473306b394 Mon Sep 17 00:00:00 2001 From: zyc <1439655764@qq.com> Date: Wed, 13 May 2026 13:44:04 +0800 Subject: [PATCH] ci: add CI/CD pipeline for cyberstar.airlabs.art - Dockerfile: multi-stage Next.js standalone build with pnpm + prisma - k8s manifests: single web deployment + Traefik ingress + LE TLS - Gitea workflow: build/push to Volcano CR, deploy to K3s, log-center failure reporting - next.config: enable standalone output for slim container image Co-Authored-By: Claude Opus 4.7 (1M context) --- .dockerignore | 21 +++++ .gitea/workflows/deploy.yaml | 171 +++++++++++++++++++++++++++++++++++ Dockerfile | 53 +++++++++++ k8s/ingress.yaml | 23 +++++ k8s/web-deployment.yaml | 75 +++++++++++++++ next.config.ts | 2 + 6 files changed, 345 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/deploy.yaml create mode 100644 Dockerfile create mode 100644 k8s/ingress.yaml create mode 100644 k8s/web-deployment.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b7be438 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,21 @@ +.git +.gitignore +.next +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* +.env +.env.* +!.env.example +.DS_Store +*.pem +coverage +.vercel +docs +README.md +.dockerignore +Dockerfile +k8s +.gitea diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml new file mode 100644 index 0000000..13d2495 --- /dev/null +++ b/.gitea/workflows/deploy.yaml @@ -0,0 +1,171 @@ +name: Build and Deploy + +on: + push: + branches: + - main + - airlabs + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + run: | + git clone --depth=1 --branch=${{ github.ref_name }} https://gitea.airlabs.art/${{ github.repository }}.git . + + - name: Set environment by branch + run: | + SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) + BUILD_DATE=$(date +%Y%m%d) + + echo "IMAGE_TAG=internal-${BUILD_DATE}-${SHORT_SHA}" >> $GITHUB_ENV + echo "CR_SERVER_ACTIVE=${{ secrets.CR_SERVER }}" >> $GITHUB_ENV + echo "CR_USERNAME_ACTIVE=${{ secrets.CR_USERNAME }}" >> $GITHUB_ENV + echo "CR_PASSWORD_ACTIVE=${{ secrets.CR_PASSWORD }}" >> $GITHUB_ENV + echo "CR_ORG=internal" >> $GITHUB_ENV + echo "DEPLOY_ENV=internal" >> $GITHUB_ENV + echo "DOMAIN_WEB=cyberstar.airlabs.art" >> $GITHUB_ENV + + - name: Login to Volcano Engine CR + run: | + echo "${{ env.CR_PASSWORD_ACTIVE }}" | docker login --username "${{ env.CR_USERNAME_ACTIVE }}" --password-stdin ${{ env.CR_SERVER_ACTIVE }} + + - name: Build and Push Web + id: build_web + run: | + set -o pipefail + for attempt in 1 2 3; do + echo "Build web attempt $attempt/3..." + DOCKER_BUILDKIT=0 docker build \ + --tag ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/cyberstar-web:${{ env.IMAGE_TAG }} \ + --tag ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/cyberstar-web:latest \ + . 2>&1 | tee /tmp/build.log && break + echo "Attempt $attempt failed, retrying in 10s..." && sleep 10 + done + for attempt in 1 2 3; do + docker push ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/cyberstar-web:${{ env.IMAGE_TAG }} && \ + docker push ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/cyberstar-web:latest && break + echo "Push attempt $attempt failed, retrying in 10s..." && sleep 10 + done + + - name: Setup Kubectl + run: | + if ! command -v kubectl &>/dev/null; then + for attempt in 1 2 3; do + curl -LO "https://files.m.daocloud.io/dl.k8s.io/release/v1.28.0/bin/linux/amd64/kubectl" && break + echo "Download attempt $attempt failed, retrying in 5s..." && sleep 5 + done + chmod +x kubectl && mv kubectl /usr/bin/kubectl + fi + kubectl version --client + + - name: Set kubeconfig + run: | + mkdir -p $HOME/.kube + printf '%s\n' '${{ secrets.VOLCANO_INTERNAL_KUBE_CONFIG }}' > $HOME/.kube/config + chmod 600 $HOME/.kube/config + echo "kubeconfig lines: $(wc -l < $HOME/.kube/config)" + grep server $HOME/.kube/config || echo "WARNING: no server found in kubeconfig" + + - name: Deploy to K3s + id: deploy + run: | + echo "Environment: ${{ env.DEPLOY_ENV }}" + CR_IMAGE="${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}" + + # Replace image placeholder + sed -i "s|\${CI_REGISTRY_IMAGE}/cyberstar-web:latest|${CR_IMAGE}/cyberstar-web:${{ env.IMAGE_TAG }}|g" k8s/web-deployment.yaml + + # Replace domain placeholder in ingress + sed -i "s|cyberstar.airlabs.art|${{ env.DOMAIN_WEB }}|g" k8s/ingress.yaml + + # Replace AUTH_URL in deployment + sed -i "s|https://cyberstar.airlabs.art|https://${{ env.DOMAIN_WEB }}|g" k8s/web-deployment.yaml + + for attempt in 1 2 3; do + echo "Deploy attempt $attempt/3..." + { + # Create/update image pull secret + kubectl create secret docker-registry cr-pull-secret \ + --docker-server="${{ env.CR_SERVER_ACTIVE }}" \ + --docker-username="${{ env.CR_USERNAME_ACTIVE }}" \ + --docker-password="${{ env.CR_PASSWORD_ACTIVE }}" \ + --dry-run=client -o yaml | kubectl apply -f - + + # Apply manifests + kubectl apply -f k8s/web-deployment.yaml + kubectl apply -f k8s/ingress.yaml + + kubectl rollout restart deployment/cyberstar-web + } 2>&1 | tee /tmp/deploy.log && break + echo "Attempt $attempt failed, retrying in 10s..." + sleep 10 + done + + - name: Report failure to Log Center + if: failure() + run: | + BUILD_LOG="" + DEPLOY_LOG="" + FAILED_STEP="unknown" + + if [[ "${{ steps.build_web.outcome }}" == "failure" ]]; then + FAILED_STEP="build" + if [ -f /tmp/build.log ]; then + BUILD_LOG=$(tail -50 /tmp/build.log | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g') + fi + elif [[ "${{ steps.deploy.outcome }}" == "failure" ]]; then + FAILED_STEP="deploy" + if [ -f /tmp/deploy.log ]; then + DEPLOY_LOG=$(tail -50 /tmp/deploy.log | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g') + fi + fi + + ERROR_LOG="${BUILD_LOG}${DEPLOY_LOG}" + if [ -z "$ERROR_LOG" ]; then + ERROR_LOG="No captured output. Check Gitea Actions UI for details." + fi + + if [[ "$FAILED_STEP" == "deploy" ]]; then + SOURCE="deployment" + ERROR_TYPE="DeployError" + else + SOURCE="cicd" + ERROR_TYPE="DockerBuildError" + fi + + curl -s -X POST "https://qiyuan-log-center-api.airlabs.art/api/v1/logs/report" \ + -H "Content-Type: application/json" \ + -d "{ + \"project_id\": \"cyberstar\", + \"environment\": \"${{ env.DEPLOY_ENV }}\", + \"level\": \"ERROR\", + \"source\": \"${SOURCE}\", + \"commit_hash\": \"${{ github.sha }}\", + \"repo_url\": \"https://gitea.airlabs.art/${{ github.repository }}.git\", + \"error\": { + \"type\": \"${ERROR_TYPE}\", + \"message\": \"[${FAILED_STEP}] Build and Deploy failed on branch ${{ github.ref_name }}\", + \"stack_trace\": [\"${ERROR_LOG}\"] + }, + \"context\": { + \"job_name\": \"build-and-deploy\", + \"step_name\": \"${FAILED_STEP}\", + \"workflow\": \"${{ github.workflow }}\", + \"run_id\": \"${{ github.run_number }}\", + \"branch\": \"${{ github.ref_name }}\", + \"actor\": \"${{ github.actor }}\", + \"commit\": \"${{ github.sha }}\", + \"run_url\": \"https://gitea.airlabs.art/${{ github.repository }}/actions/runs/${{ github.run_number }}\" + } + }" || true + + - name: Docker Cleanup + if: always() + run: | + docker container prune -f + docker image prune -f + docker builder prune -a -f + echo "Disk usage after cleanup:" + df -h / | tail -1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1abf877 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,53 @@ +# syntax=docker/dockerfile:1 +# ───────────── 1. deps:安装依赖(含 prisma generate via postinstall) ───────────── +FROM node:20-alpine AS deps +RUN apk add --no-cache libc6-compat openssl +WORKDIR /app + +RUN corepack enable && corepack prepare pnpm@latest --activate \ + && pnpm config set registry https://registry.npmmirror.com + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY prisma ./prisma +RUN pnpm install --frozen-lockfile + +# ───────────── 2. builder:Next.js 构建(standalone 产物) ───────────── +FROM node:20-alpine AS builder +RUN apk add --no-cache libc6-compat openssl +WORKDIR /app + +RUN corepack enable && corepack prepare pnpm@latest --activate + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +ENV NEXT_TELEMETRY_DISABLED=1 +RUN pnpm build + +# ───────────── 3. runner:最小运行时镜像 ───────────── +FROM node:20-alpine AS runner +RUN apk add --no-cache libc6-compat openssl +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 + +RUN addgroup --system --gid 1001 nodejs \ + && adduser --system --uid 1001 nextjs + +# Next.js standalone 输出 + 静态资源 + public +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +# Prisma:生成的 client + schema(运行时 db:push / 迁移可能用到) +COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma +COPY --from=builder --chown=nextjs:nodejs /app/node_modules/@prisma ./node_modules/@prisma +COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma + +USER nextjs +EXPOSE 3000 + +CMD ["node", "server.js"] diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml new file mode 100644 index 0000000..62204e6 --- /dev/null +++ b/k8s/ingress.yaml @@ -0,0 +1,23 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: cyberstar-ingress + annotations: + kubernetes.io/ingress.class: "traefik" + cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + tls: + - hosts: + - cyberstar.airlabs.art + secretName: cyberstar-tls + rules: + - host: cyberstar.airlabs.art + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: cyberstar-web + port: + number: 80 diff --git a/k8s/web-deployment.yaml b/k8s/web-deployment.yaml new file mode 100644 index 0000000..abb9274 --- /dev/null +++ b/k8s/web-deployment.yaml @@ -0,0 +1,75 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cyberstar-web + labels: + app: cyberstar-web +spec: + replicas: 1 + selector: + matchLabels: + app: cyberstar-web + template: + metadata: + labels: + app: cyberstar-web + spec: + imagePullSecrets: + - name: cr-pull-secret + containers: + - name: cyberstar-web + image: ${CI_REGISTRY_IMAGE}/cyberstar-web:latest + imagePullPolicy: Always + ports: + - containerPort: 3000 + env: + - name: NODE_ENV + value: "production" + - name: PORT + value: "3000" + - name: HOSTNAME + value: "0.0.0.0" + - name: AUTH_URL + value: "https://cyberstar.airlabs.art" + - name: AUTH_TRUST_HOST + value: "true" + # 敏感配置 / 第三方凭据从 Secret 注入(部署前需 kubectl create secret generic cyberstar-env --from-env-file=.env) + envFrom: + - secretRef: + name: cyberstar-env + optional: true + livenessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + requests: + memory: "256Mi" + cpu: "200m" + limits: + memory: "1024Mi" + cpu: "1000m" +--- +apiVersion: v1 +kind: Service +metadata: + name: cyberstar-web +spec: + selector: + app: cyberstar-web + ports: + - protocol: TCP + port: 80 + targetPort: 3000 diff --git a/next.config.ts b/next.config.ts index c1c3650..3020ec5 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,6 +3,8 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { // 关闭左下角的开发指示器(dev overlay 角标) devIndicators: false, + // 容器化部署:产出精简的 standalone 包(node server.js 启动) + output: "standalone", }; export default nextConfig;