Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1210db1a7 | |||
| 68d5ca119a | |||
| 4bda9d6ba2 | |||
| 0a82bac723 | |||
| 1efc7581e6 | |||
| 29b879b274 | |||
| ec05269806 | |||
| ff8d4fefe2 | |||
| 835ebafc21 | |||
| 02cccbd94b | |||
| bc5880ec31 | |||
| 38ab924b91 | |||
| 021cb81125 | |||
| ec157b06dc | |||
| bc707179ce | |||
| e1faacf7a7 | |||
| 2a24c16244 | |||
| 9c49e21575 | |||
| f712fa9e85 | |||
| 88747cbd48 | |||
| 131e25c154 |
@@ -0,0 +1,172 @@
|
||||
name: lpt-fe Build & Deploy
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
description: '部署环境(dev 仅允许 dev 分支;prod 仅允许 master/main)'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- dev
|
||||
- prod
|
||||
|
||||
env:
|
||||
REGISTRY: 192.168.123.199:5000
|
||||
APP: lpt-fe
|
||||
REPO_URL: http://git.cat-shark.xyz/cat-shark/lpt-fe.git
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Validate branch ↔ environment
|
||||
run: |
|
||||
set -euo pipefail
|
||||
REF="${{ gitea.ref }}"
|
||||
BRANCH="${REF#refs/heads/}"
|
||||
# tag 触发时拒绝
|
||||
if [[ "$REF" == refs/tags/* ]]; then
|
||||
echo "ERROR: 请从分支触发部署,不要用 tag。当前 ref=$REF"
|
||||
exit 1
|
||||
fi
|
||||
ENV="${{ inputs.environment }}"
|
||||
echo "branch=$BRANCH environment=$ENV sha=${{ gitea.sha }}"
|
||||
case "$ENV" in
|
||||
prod)
|
||||
case "$BRANCH" in
|
||||
master|main) echo "OK: prod 允许从 $BRANCH 部署" ;;
|
||||
*)
|
||||
echo "ERROR: prod 只能从 master/main 部署,当前分支是 '$BRANCH'"
|
||||
echo "请切换到 master 后再 Run workflow,并选择 environment=prod"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
dev)
|
||||
case "$BRANCH" in
|
||||
dev) echo "OK: dev 允许从 $BRANCH 部署" ;;
|
||||
*)
|
||||
echo "ERROR: dev 只能从 dev 分支部署,当前分支是 '$BRANCH'"
|
||||
echo "请切换到 dev 后再 Run workflow,并选择 environment=dev"
|
||||
echo "(禁止用 master 代码部署到 lpt-dev,避免环境错配)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: 未知 environment=$ENV"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Checkout code
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git clone "$REPO_URL" .
|
||||
git checkout "${{ gitea.sha }}"
|
||||
|
||||
- name: Login to Docker Registry
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "$REGISTRY" -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Build & Push Docker image
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ENV="${{ inputs.environment }}"
|
||||
if [ "$ENV" = "prod" ]; then
|
||||
BUILD_MODE=production
|
||||
else
|
||||
BUILD_MODE=development
|
||||
fi
|
||||
TAG="${{ gitea.sha }}-$(date +%s)"
|
||||
echo "Building $REGISTRY/$APP:$TAG (BUILD_MODE=$BUILD_MODE, env tag=$ENV)"
|
||||
docker build \
|
||||
--build-arg "BUILD_MODE=$BUILD_MODE" \
|
||||
-t "$REGISTRY/$APP:$TAG" \
|
||||
-t "$REGISTRY/$APP:$ENV" .
|
||||
docker push "$REGISTRY/$APP:$TAG"
|
||||
docker push "$REGISTRY/$APP:$ENV"
|
||||
# 持久化 TAG:兼容 Gitea / GitHub Actions;文件兜底避免 env 文件不支持
|
||||
if [ -n "${GITHUB_ENV:-}" ]; then
|
||||
echo "IMAGE_TAG=$TAG" >> "$GITHUB_ENV"
|
||||
fi
|
||||
if [ -n "${GITEA_ENV:-}" ]; then
|
||||
echo "IMAGE_TAG=$TAG" >> "$GITEA_ENV"
|
||||
fi
|
||||
echo "$TAG" > image_tag.txt
|
||||
if [ -n "${{ runner.temp }}" ]; then
|
||||
echo "$TAG" > "${{ runner.temp }}/image_tag.txt" || true
|
||||
fi
|
||||
echo "IMAGE_TAG=$TAG"
|
||||
|
||||
- name: Setup kubectl
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -sLO "https://dl.k8s.io/release/v1.30.0/bin/linux/amd64/kubectl"
|
||||
chmod +x kubectl
|
||||
mv kubectl /usr/local/bin/
|
||||
echo "${{ secrets.KUBECONFIG_B64 }}" | base64 -d > /tmp/kubeconfig
|
||||
|
||||
- name: Create/Update imagePullSecret
|
||||
run: |
|
||||
set -euo pipefail
|
||||
kubectl --kubeconfig=/tmp/kubeconfig create secret docker-registry regcred \
|
||||
--docker-server="$REGISTRY" \
|
||||
--docker-username="${{ secrets.REGISTRY_USERNAME }}" \
|
||||
--docker-password="${{ secrets.REGISTRY_PASSWORD }}" \
|
||||
-n "lpt-${{ inputs.environment }}" \
|
||||
--dry-run=client -o yaml | kubectl --kubeconfig=/tmp/kubeconfig apply -f -
|
||||
|
||||
- name: Deploy to K8s
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ENV="${{ inputs.environment }}"
|
||||
IMAGE_TAG="${IMAGE_TAG:-}"
|
||||
if [ -z "$IMAGE_TAG" ] && [ -f image_tag.txt ]; then
|
||||
IMAGE_TAG="$(cat image_tag.txt)"
|
||||
fi
|
||||
if [ -z "$IMAGE_TAG" ] && [ -f "${{ runner.temp }}/image_tag.txt" ]; then
|
||||
IMAGE_TAG="$(cat "${{ runner.temp }}/image_tag.txt")"
|
||||
fi
|
||||
if [ -z "$IMAGE_TAG" ]; then
|
||||
echo "ERROR: IMAGE_TAG 为空,无法部署"
|
||||
exit 1
|
||||
fi
|
||||
echo "Deploying $REGISTRY/$APP:$IMAGE_TAG -> namespace lpt-$ENV"
|
||||
kubectl --kubeconfig=/tmp/kubeconfig set image "deployment/$APP" \
|
||||
"$APP=$REGISTRY/$APP:$IMAGE_TAG" \
|
||||
-n "lpt-$ENV" --record
|
||||
kubectl --kubeconfig=/tmp/kubeconfig rollout status "deployment/$APP" \
|
||||
-n "lpt-$ENV" --timeout=5m
|
||||
|
||||
- name: Debug on failure
|
||||
if: failure()
|
||||
run: |
|
||||
ENV="${{ inputs.environment }}"
|
||||
echo "=== Deployment Status ==="
|
||||
kubectl --kubeconfig=/tmp/kubeconfig get deployment "$APP" -n "lpt-$ENV" || true
|
||||
echo ""
|
||||
echo "=== Pod Status ==="
|
||||
kubectl --kubeconfig=/tmp/kubeconfig get pods -n "lpt-$ENV" -l "app=$APP" || true
|
||||
echo ""
|
||||
echo "=== Recent Events ==="
|
||||
kubectl --kubeconfig=/tmp/kubeconfig get events -n "lpt-$ENV" --sort-by='.lastTimestamp' | tail -20 || true
|
||||
echo ""
|
||||
echo "=== Pod Describe (latest) ==="
|
||||
POD=$(kubectl --kubeconfig=/tmp/kubeconfig get pods -n "lpt-$ENV" -l "app=$APP" --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1].metadata.name}' 2>/dev/null || true)
|
||||
if [ -n "${POD:-}" ]; then
|
||||
kubectl --kubeconfig=/tmp/kubeconfig describe pod "$POD" -n "lpt-$ENV" || true
|
||||
echo ""
|
||||
echo "=== Pod Logs (latest) ==="
|
||||
kubectl --kubeconfig=/tmp/kubeconfig logs "$POD" -n "lpt-$ENV" --tail=50 || true
|
||||
fi
|
||||
|
||||
- name: Rollback on failure
|
||||
if: failure()
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ENV="${{ inputs.environment }}"
|
||||
kubectl --kubeconfig=/tmp/kubeconfig rollout undo "deployment/$APP" -n "lpt-$ENV" || true
|
||||
kubectl --kubeconfig=/tmp/kubeconfig rollout status "deployment/$APP" -n "lpt-$ENV" || true
|
||||
@@ -0,0 +1,72 @@
|
||||
# 部署流水线问题排查进度
|
||||
|
||||
**日期**: 2026-07-20(更新)
|
||||
|
||||
## 当前部署方式
|
||||
|
||||
- **Jenkinsfile / Jenkinsfile-dev**:历史方案,已不再作为主路径
|
||||
- **现行方案**:Gitea Actions(`.gitea/workflows/deploy.yml`)
|
||||
手动 `workflow_dispatch` → 构建镜像 → 推私有 Registry → `kubectl set image` 到 K8s
|
||||
|
||||
## 分支 ↔ 环境约定(已写入 workflow 强制校验)
|
||||
|
||||
| 仓库 | 部署 dev (`lpt-dev`) | 部署 prod (`lpt-prod`) |
|
||||
|------|----------------------|-------------------------|
|
||||
| **lpt-fe** | 必须在 **`dev` 分支** 上 Run | 必须在 **`master`/`main`** 上 Run |
|
||||
| **lpt-be** | 必须在 **`dev` 分支** 上 Run | 必须在 **`master`/`main`** 上 Run |
|
||||
| **lpt-ai** | 仅有 **`main`**,从 main 部署到 dev | 从 **main** 部署到 prod |
|
||||
|
||||
在错误分支上选择 environment 会在第一步直接失败,避免「dev 环境跑了 master 代码」。
|
||||
|
||||
### 正确操作
|
||||
|
||||
1. Gitea 仓库页面切换到目标分支(fe/be 的 dev 或 master)
|
||||
2. Actions → 对应 workflow → Run workflow
|
||||
3. 选择 `environment` = `dev` 或 `prod`(须与当前分支匹配)
|
||||
|
||||
## 已修复项(2026-07-20)
|
||||
|
||||
1. **分支与环境绑定校验** — 三个仓库的 `deploy.yml` 增加 Validate 步骤
|
||||
2. **`IMAGE_TAG` 持久化** — 除 `GITHUB_ENV` 外,写入 `GITEA_ENV`(若存在)+ 临时文件兜底,Deploy 步读取失败则明确报错
|
||||
3. **fe `BUILD_MODE`** — 在 shell 内根据 environment 设置 `production` / `development`,避免表达式兼容问题
|
||||
4. **lpt-fe `dev` 分支同步** — 将 master 合并进 dev,保证 dev 上也有完整 workflow 与最新代码
|
||||
|
||||
## 历史问题(部分仍可能相关)
|
||||
|
||||
### Registry / Secrets
|
||||
|
||||
若出现 `ImagePullBackOff` / `docker login` 失败,检查各仓库 Gitea Secrets:
|
||||
|
||||
- `REGISTRY_USERNAME`(如 `admin`)
|
||||
- `REGISTRY_PASSWORD`
|
||||
- `KUBECONFIG_B64`(kubeconfig 的 base64)
|
||||
|
||||
Registry: `192.168.123.199:5000`
|
||||
|
||||
### K8s
|
||||
|
||||
- 共享清单:`k8s/dev/*`、`k8s/prod/*`(按环境)
|
||||
- 各服务仓库内 `k8s/` 仍多为 `lpt-dev` 模板;日常 CI 只 `set image`,不 `apply` 整份 YAML
|
||||
- Deployment 需有 `imagePullSecrets: [regcred]`;workflow 每次会 create/update `regcred`
|
||||
|
||||
## 镜像与命名空间
|
||||
|
||||
| environment | 镜像 tag(浮动) | 不可变 tag | namespace |
|
||||
|-------------|------------------|------------|-----------|
|
||||
| dev | `app:dev` | `app:<sha>-<unix>` | `lpt-dev` |
|
||||
| prod | `app:prod` | `app:<sha>-<unix>` | `lpt-prod` |
|
||||
|
||||
部署使用不可变 tag,避免 `imagePullPolicy` 与缓存导致未更新。
|
||||
|
||||
## 验证清单
|
||||
|
||||
1. [ ] 三仓库 Secrets 已配置
|
||||
2. [ ] lpt-fe:在 **dev** 分支 Run → environment=dev 成功
|
||||
3. [ ] lpt-fe:在 **master** 上选 environment=dev 应 **失败**(校验)
|
||||
4. [ ] lpt-be:同上
|
||||
5. [ ] lpt-ai:在 **main** 上 Run → dev / prod
|
||||
6. [ ] `kubectl get pods -n lpt-dev` / `lpt-prod` 正常
|
||||
|
||||
---
|
||||
|
||||
**备注**: 主路径为 Gitea Actions + K8s;Jenkins 配置可保留作参考,勿与现行流程混用。
|
||||
Vendored
+8
-2
@@ -1,7 +1,7 @@
|
||||
pipeline {
|
||||
agent none
|
||||
environment {
|
||||
IMAGE_NAME = 'lpt-fe:0.0'
|
||||
IMAGE_NAME = "lpt-fe:${env.GIT_COMMIT?.take(8) ?: '0.0'}"
|
||||
CONTAINER_NAME = 'LPT_FE'
|
||||
CONTAINER_PORT = '80'
|
||||
}
|
||||
@@ -34,7 +34,13 @@ pipeline {
|
||||
stage('健康检查') {
|
||||
agent any
|
||||
steps {
|
||||
sh 'sleep 5 && docker exec $CONTAINER_NAME curl -f http://localhost:$CONTAINER_PORT/ || (docker logs $CONTAINER_NAME && exit 1)'
|
||||
sh 'sleep 15 && docker exec $CONTAINER_NAME curl -f http://localhost:$CONTAINER_PORT/ || (docker logs $CONTAINER_NAME && exit 1)'
|
||||
}
|
||||
}
|
||||
stage('清理旧镜像') {
|
||||
agent any
|
||||
steps {
|
||||
sh 'docker image prune -af --filter "until=168h" || true'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: lpt-fe
|
||||
namespace: lpt-dev
|
||||
labels:
|
||||
app: lpt-fe
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: lpt-fe
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 25%
|
||||
maxUnavailable: 25%
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: lpt-fe
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: regcred
|
||||
containers:
|
||||
- name: lpt-fe
|
||||
image: 192.168.123.199:5000/lpt-fe:dev
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 80
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 128Mi
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 80
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 15
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 80
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: lpt-fe
|
||||
namespace: lpt-dev
|
||||
labels:
|
||||
app: lpt-fe
|
||||
spec:
|
||||
type: NodePort
|
||||
selector:
|
||||
app: lpt-fe
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
nodePort: 30080
|
||||
protocol: TCP
|
||||
@@ -6,6 +6,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite --mode development",
|
||||
"build:dev": "vite build --mode development",
|
||||
"build:development": "vite build --mode development",
|
||||
"build:production": "vite build --mode production",
|
||||
"build": "vite build --mode production",
|
||||
"preview": "vite preview",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import request from "@/utils/request";
|
||||
import type { ReviewMindMap } from "./review";
|
||||
|
||||
// ============ 标准思维导图 ============
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ const findNodeByPath = (node: MindMapTreeNode, path: string): MindMapTreeNode |
|
||||
let current: MindMapTreeNode | null = node;
|
||||
for (let i = 0; i < parts.length && current; i++) {
|
||||
if (current.title === parts[i] && i === parts.length - 1) return current;
|
||||
const child = (current.children || []).find((c) => c.title === parts[i]);
|
||||
const child: MindMapTreeNode | undefined = (current.children || []).find((c) => c.title === parts[i]);
|
||||
current = child || null;
|
||||
}
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user