From 9bcf943e82b882b758d52bd27fd4e94673f5ec38 Mon Sep 17 00:00:00 2001 From: zyc <1439655764@qq.com> Date: Fri, 15 May 2026 18:12:56 +0800 Subject: [PATCH] =?UTF-8?q?ci:=20=E6=B7=BB=E5=8A=A0=20Gitea=20Actions=20?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E6=B5=81=E6=B0=B4=E7=BA=BF=20+=20K3s=20?= =?UTF-8?q?=E6=B8=85=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对齐 jimeng-clone 的流水线结构(master→prod / dev→dev),适配 AirShelf 纯静态形态: - 电商AI平台/Dockerfile · 纯 nginx,无 build 阶段 - 电商AI平台/nginx.conf · 多入口 try_files,不 fallback 到 index.html - k8s/ · web-deployment + ingress + cert-issuer + redirect middleware - .gitea/workflows/deploy.yaml · build/push 重试 3 次,deploy 重试 5 次,失败上报日志中心 - prod 域名 airshelf.airlabs.art / dev 域名 airshelf.test.airlabs.art Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitea/workflows/deploy.yaml | 200 +++++++++++++++++++++++++++++ README.md | 20 +++ k8s/cert-manager-issuer.yaml | 15 +++ k8s/ingress.yaml | 24 ++++ k8s/redirect-https-middleware.yaml | 8 ++ k8s/web-deployment.yaml | 59 +++++++++ 电商AI平台/.dockerignore | 7 + 电商AI平台/Dockerfile | 10 ++ 电商AI平台/nginx.conf | 28 ++++ 9 files changed, 371 insertions(+) create mode 100644 .gitea/workflows/deploy.yaml create mode 100644 k8s/cert-manager-issuer.yaml create mode 100644 k8s/ingress.yaml create mode 100644 k8s/redirect-https-middleware.yaml create mode 100644 k8s/web-deployment.yaml create mode 100644 电商AI平台/.dockerignore create mode 100644 电商AI平台/Dockerfile create mode 100644 电商AI平台/nginx.conf diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml new file mode 100644 index 0000000..78e3255 --- /dev/null +++ b/.gitea/workflows/deploy.yaml @@ -0,0 +1,200 @@ +name: Build and Deploy + +on: + push: + branches: + - master + - dev + +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) + + if [[ "${{ github.ref_name }}" == "master" ]]; then + echo "IMAGE_TAG=prod-${BUILD_DATE}-${SHORT_SHA}" >> $GITHUB_ENV + echo "CR_SERVER_ACTIVE=gitea-prod-cn-shanghai.cr.volces.com" >> $GITHUB_ENV + echo "CR_USERNAME_ACTIVE=seaislee@76339115" >> $GITHUB_ENV + echo "CR_PASSWORD_ACTIVE=${{ secrets.CR_PROD_PASSWORD }}" >> $GITHUB_ENV + echo "CR_ORG=prod" >> $GITHUB_ENV + echo "DEPLOY_ENV=production" >> $GITHUB_ENV + echo "DOMAIN_WEB=airshelf.airlabs.art" >> $GITHUB_ENV + elif [[ "${{ github.ref_name }}" == "dev" ]]; then + echo "IMAGE_TAG=dev-${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=dev" >> $GITHUB_ENV + echo "DEPLOY_ENV=development" >> $GITHUB_ENV + echo "DOMAIN_WEB=airshelf.test.airlabs.art" >> $GITHUB_ENV + fi + + - 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 + ok=0 + 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 }}/airshelf-web:${{ env.IMAGE_TAG }} \ + --tag ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/airshelf-web:latest \ + "./电商AI平台" 2>&1 | tee /tmp/build.log && { ok=1; break; } + echo "Attempt $attempt failed, retrying in 10s..." && sleep 10 + done + [ $ok -eq 1 ] || { echo "ERROR: web build failed after 3 attempts"; exit 1; } + ok=0 + for attempt in 1 2 3; do + docker push ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/airshelf-web:${{ env.IMAGE_TAG }} && \ + docker push ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/airshelf-web:latest && { ok=1; break; } + echo "Push attempt $attempt failed, retrying in 10s..." && sleep 10 + done + [ $ok -eq 1 ] || { echo "ERROR: web push failed after 3 attempts"; exit 1; } + + - name: Setup Kubectl + run: | + if ! command -v kubectl &>/dev/null; then + ok=0 + 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" && { ok=1; break; } + echo "Download attempt $attempt failed, retrying in 5s..." && sleep 5 + done + [ $ok -eq 1 ] || { echo "ERROR: kubectl download failed after 3 attempts"; exit 1; } + chmod +x kubectl && mv kubectl /usr/bin/kubectl + fi + kubectl version --client + + - name: Set kubeconfig + run: | + mkdir -p $HOME/.kube + if [[ "${{ github.ref_name }}" == "master" ]]; then + printf '%s\n' '${{ secrets.VOLCANO_PROD_KUBE_CONFIG }}' > $HOME/.kube/config + elif [[ "${{ github.ref_name }}" == "dev" ]]; then + printf '%s\n' '${{ secrets.VOLCANO_TEST_KUBE_CONFIG }}' > $HOME/.kube/config + fi + 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 placeholders + sed -i "s|\${CI_REGISTRY_IMAGE}/airshelf-web:latest|${CR_IMAGE}/airshelf-web:${{ env.IMAGE_TAG }}|g" k8s/web-deployment.yaml + + # Replace domain placeholder in ingress + sed -i "s|airshelf.airlabs.art|${{ env.DOMAIN_WEB }}|g" k8s/ingress.yaml + + # All kubectl operations with retry (K3s 内网连接可能抖动) + export KUBECTL_TIMEOUT="--request-timeout=4s" + + ok=0 + for attempt in 1 2 3 4 5; do + echo "Deploy attempt $attempt/5..." + { + # Create/update image pull secret for CR + kubectl $KUBECTL_TIMEOUT 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 $KUBECTL_TIMEOUT apply -f - + + # Apply manifests + kubectl $KUBECTL_TIMEOUT apply -f k8s/cert-manager-issuer.yaml + kubectl $KUBECTL_TIMEOUT apply -f k8s/redirect-https-middleware.yaml + kubectl $KUBECTL_TIMEOUT apply -f k8s/web-deployment.yaml + kubectl $KUBECTL_TIMEOUT apply -f k8s/ingress.yaml + + # Preserve real client IP + kubectl $KUBECTL_TIMEOUT patch svc traefik -n kube-system -p '{"spec":{"externalTrafficPolicy":"Local"}}' 2>/dev/null || true + + kubectl $KUBECTL_TIMEOUT rollout restart deployment/airshelf-web + } 2>&1 | tee /tmp/deploy.log && { ok=1; break; } + echo "Attempt $attempt failed, retrying in 30s..." + sleep 30 + done + [ $ok -eq 1 ] || { echo "ERROR: deploy to K3s failed after 5 attempts — check /tmp/deploy.log"; exit 1; } + + # ===== Log Center: failure reporting ===== + - 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\": \"airshelf\", + \"environment\": \"${{ env.DEPLOY_ENV }}\", + \"level\": \"ERROR\", + \"source\": \"${SOURCE}\", + \"commit_hash\": \"${{ github.sha }}\", + \"repo_url\": \"https://gitea.airlabs.art/zyc/AirShelf.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 + + # ===== Cleanup: remove unused Docker resources ===== + - 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/README.md b/README.md index da86bbc..476bf2c 100644 --- a/README.md +++ b/README.md @@ -31,3 +31,23 @@ AirShelf/ ├── <未来项目 B>/ └── <未来项目 C>/ ``` + +--- + +## 部署 + +CI/CD 走 Gitea Actions + 火山引擎 CR + K3s(traefik + cert-manager)。 + +| 分支 | 环境 | 域名 | Image tag | +| --- | --- | --- | --- | +| `master` | production | `airshelf.airlabs.art` | `prod-YYYYMMDD-` | +| `dev` | development | `airshelf.test.airlabs.art` | `dev-YYYYMMDD-` | + +推到对应分支会自动触发 [.gitea/workflows/deploy.yaml](.gitea/workflows/deploy.yaml): +checkout → docker build/push (`airshelf-web`,无构建阶段、纯 nginx + 静态) → kubectl apply [k8s/](k8s/) → rollout restart。 + +构建上下文是 `电商AI平台/`,Dockerfile/nginx.conf 都在该子目录。当前仅一个项目,故 image 名固定 `airshelf-web`;若未来加兄弟项目,流水线需要扩展为按项目分别构建。 + +**Gitea 仓库需要配置的 Secrets:** +- prod: `CR_PROD_PASSWORD` · `VOLCANO_PROD_KUBE_CONFIG` +- dev: `CR_SERVER` · `CR_USERNAME` · `CR_PASSWORD` · `VOLCANO_TEST_KUBE_CONFIG` diff --git a/k8s/cert-manager-issuer.yaml b/k8s/cert-manager-issuer.yaml new file mode 100644 index 0000000..b78a0e3 --- /dev/null +++ b/k8s/cert-manager-issuer.yaml @@ -0,0 +1,15 @@ +# ClusterIssuer for Let's Encrypt automatic certificate generation & renewal +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: airlabsv001@gmail.com + privateKeySecretRef: + name: letsencrypt-prod-key + solvers: + - http01: + ingress: + class: traefik diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml new file mode 100644 index 0000000..c31894e --- /dev/null +++ b/k8s/ingress.yaml @@ -0,0 +1,24 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: airshelf-ingress + annotations: + kubernetes.io/ingress.class: "traefik" + cert-manager.io/cluster-issuer: "letsencrypt-prod" + traefik.ingress.kubernetes.io/router.middlewares: "default-redirect-https@kubernetescrd" +spec: + tls: + - hosts: + - airshelf.airlabs.art + secretName: airshelf-tls + rules: + - host: airshelf.airlabs.art + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: airshelf-web + port: + number: 80 diff --git a/k8s/redirect-https-middleware.yaml b/k8s/redirect-https-middleware.yaml new file mode 100644 index 0000000..e5eedb9 --- /dev/null +++ b/k8s/redirect-https-middleware.yaml @@ -0,0 +1,8 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: redirect-https +spec: + redirectScheme: + scheme: https + permanent: true diff --git a/k8s/web-deployment.yaml b/k8s/web-deployment.yaml new file mode 100644 index 0000000..ff9091f --- /dev/null +++ b/k8s/web-deployment.yaml @@ -0,0 +1,59 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: airshelf-web + labels: + app: airshelf-web +spec: + replicas: 1 + selector: + matchLabels: + app: airshelf-web + template: + metadata: + labels: + app: airshelf-web + spec: + imagePullSecrets: + - name: cr-pull-secret + containers: + - name: airshelf-web + image: ${CI_REGISTRY_IMAGE}/airshelf-web:latest + imagePullPolicy: Always + ports: + - containerPort: 80 + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + requests: + memory: "32Mi" + cpu: "20m" + limits: + memory: "128Mi" + cpu: "150m" +--- +apiVersion: v1 +kind: Service +metadata: + name: airshelf-web +spec: + selector: + app: airshelf-web + ports: + - protocol: TCP + port: 80 + targetPort: 80 diff --git a/电商AI平台/.dockerignore b/电商AI平台/.dockerignore new file mode 100644 index 0000000..bc81268 --- /dev/null +++ b/电商AI平台/.dockerignore @@ -0,0 +1,7 @@ +Dockerfile +.dockerignore +.git +.gitignore +.DS_Store +*.md +node_modules diff --git a/电商AI平台/Dockerfile b/电商AI平台/Dockerfile new file mode 100644 index 0000000..be764df --- /dev/null +++ b/电商AI平台/Dockerfile @@ -0,0 +1,10 @@ +# ---- Runtime Stage (no build — pure static HTML/CSS/JS) ---- +FROM docker.m.daocloud.io/nginx:alpine + +RUN sed -i 's#dl-cdn.alpinelinux.org#mirrors.aliyun.com#g' /etc/apk/repositories + +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY . /usr/share/nginx/html/ + +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/电商AI平台/nginx.conf b/电商AI平台/nginx.conf new file mode 100644 index 0000000..106342f --- /dev/null +++ b/电商AI平台/nginx.conf @@ -0,0 +1,28 @@ +server_tokens off; +charset utf-8; + +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; + + # Cache static assets (CSS/JS/images/fonts) + location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|mp4|webm)$ { + expires 30d; + add_header Cache-Control "public, immutable"; + } + + # Multi-page (not SPA): try exact file, then .html fallback (friendly URLs), then 404. + # NEVER fallback to index.html — that masks 404s for a multi-entry design shelf. + location / { + try_files $uri $uri/ $uri.html =404; + } +}