diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..ef23c9c --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,71 @@ +# Gitea Actions workflow for Node/Vue/React frontend +# Place at: /.gitea/workflows/build.yml + +name: Build & Deploy Frontend + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + REGISTRY: 127.0.0.1:5000 + IMAGE_NAME: web-frontend + +jobs: + build: + runs-on: k3s-runner-01 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci || npm install + + - name: Build + run: npm run build + env: + # 注入构建时环境变量 + VITE_API_URL: ${{ vars.API_URL }} + + - name: Build & push Docker image + env: + REGISTRY: ${{ env.REGISTRY }} + run: | + IMAGE_TAG="${{ env.REGISTRY }}/${IMAGE_NAME}:${GITHUB_SHA::8}" + docker build \ + --build-arg VITE_API_URL="${{ vars.API_URL }}" \ + -t "${IMAGE_TAG}" \ + -t "${REGISTRY}/${IMAGE_NAME}:latest" \ + -f Dockerfile . + docker push "${IMAGE_TAG}" + docker push "${REGISTRY}/${IMAGE_NAME}:latest" + echo "IMAGE_TAG=${IMAGE_TAG}" >> $GITHUB_OUTPUT + echo "DEPLOY_IMAGE=${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA::8}" >> $GITHUB_ENV + + - name: Update ArgoCD app + run: | + # 触发 ArgoCD 重新同步 + curl -sk -X POST \ + -H "Authorization: Bearer ${{ secrets.ARGOCD_TOKEN }}" \ + "https://argocd.k3s-runner.cicd.svc.cluster.local/api/v1/applications/web-frontend/restart" + + deploy: + needs: build + runs-on: k3s-runner-01 + if: github.ref == 'refs/heads/main' + steps: + - name: Trigger ArgoCD sync + env: + IMAGE: ${{ needs.build.outputs.IMAGE_TAG }} + run: | + # Patch ArgoCD Application 指向新镜像 + kubectl -n argocd patch application web-frontend --type merge \ + -p "{\"spec\":{\"source\":{\"helm\":{\"parameters\":[{\"name\":\"image.tag\",\"value\":\"${GITHUB_SHA::8}\"}]}}}}" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..885128b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +# Frontend Dockerfile (nginx + 静态产物) +# 多阶段构建 + +FROM node:20-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci || npm install +COPY . . +RUN npm run build + +FROM nginx:1.27-alpine +COPY --from=builder /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +HEALTHCHECK --interval=30s --timeout=3s CMD wget -q -O- http://localhost/ || exit 1 +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/deploy/helm/Chart.yaml b/deploy/helm/Chart.yaml new file mode 100644 index 0000000..84a5675 --- /dev/null +++ b/deploy/helm/Chart.yaml @@ -0,0 +1,9 @@ +# Minimal Helm chart for any service +# Place at: /deploy/helm/ + +apiVersion: v2 +name: demo-frontend +description: Standard Helm chart for k3s services +type: application +version: 0.1.0 +appVersion: "1.0.0" \ No newline at end of file diff --git a/deploy/helm/templates/deployment.yaml b/deploy/helm/templates/deployment.yaml new file mode 100644 index 0000000..c663ca0 --- /dev/null +++ b/deploy/helm/templates/deployment.yaml @@ -0,0 +1,85 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Chart.Name }} + labels: + app: {{ .Chart.Name }} + app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ .Chart.Name }} + template: + metadata: + labels: + app: {{ .Chart.Name }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.registry }}/{{ .Values.image.name }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- if .Values.probes.liveness.enabled }} + livenessProbe: + httpGet: + path: {{ .Values.probes.liveness.path }} + port: {{ .Values.probes.liveness.port }} + initialDelaySeconds: 30 + periodSeconds: 10 + {{- end }} + {{- if .Values.probes.readiness.enabled }} + readinessProbe: + httpGet: + path: {{ .Values.probes.readiness.path }} + port: {{ .Values.probes.readiness.port }} + initialDelaySeconds: 5 + periodSeconds: 5 + {{- end }} + env: + {{- toYaml .Values.env | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Chart.Name }} +spec: + type: {{ .Values.service.type }} + selector: + app: {{ .Chart.Name }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http +{{- if .Values.ingress.enabled }} +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ .Chart.Name }} + annotations: + {{- if .Values.ingress.className }} + kubernetes.io/ingress.class: {{ .Values.ingress.className }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + rules: + - host: {{ .Values.ingress.host }} + http: + paths: + - path: {{ .Values.ingress.path | default "/" }} + pathType: Prefix + backend: + service: + name: {{ .Chart.Name }} + port: + number: {{ .Values.service.port }} +{{- end }} \ No newline at end of file diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml new file mode 100644 index 0000000..0e923af --- /dev/null +++ b/deploy/helm/values.yaml @@ -0,0 +1,38 @@ +# Default values +image: + registry: 127.0.0.1:5000 + name: demo-frontend + tag: latest + pullPolicy: Always + +replicaCount: 2 + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: traefik + host: demo-frontend.local + path: / + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + +env: [] + +probes: + liveness: + enabled: true + path: /healthz + port: 8080 + readiness: + enabled: true + path: /healthz + port: 8080 \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..22dac66 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,23 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # SPA fallback + location / { + try_files $uri $uri/ /index.html; + } + + # 静态资源缓存 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # 健康检查 + location /healthz { + access_log off; + return 200 "ok\n"; + } +} \ No newline at end of file