fix(ci): 绑定分支与部署环境并加固 IMAGE_TAG 传递
- dev 仅允许 dev 分支,prod 仅允许 master/main,防止 master 代码进 lpt-dev - IMAGE_TAG 同时写 GITHUB_ENV/GITEA_ENV 与工作区文件兜底 - 更新部署说明文档 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+110
-25
@@ -4,7 +4,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
environment:
|
environment:
|
||||||
description: '部署环境'
|
description: '部署环境(dev 仅允许 dev 分支;prod 仅允许 master/main)'
|
||||||
required: true
|
required: true
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
@@ -20,28 +20,90 @@ jobs:
|
|||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
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
|
- name: Checkout code
|
||||||
run: |
|
run: |
|
||||||
git clone $REPO_URL .
|
set -euo pipefail
|
||||||
git checkout ${{ gitea.sha }}
|
git clone "$REPO_URL" .
|
||||||
|
git checkout "${{ gitea.sha }}"
|
||||||
|
|
||||||
- name: Login to Docker Registry
|
- name: Login to Docker Registry
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login $REGISTRY -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
|
set -euo pipefail
|
||||||
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "$REGISTRY" -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
|
||||||
|
|
||||||
- name: Build & Push Docker image
|
- name: Build & Push Docker image
|
||||||
run: |
|
run: |
|
||||||
TAG=${{ gitea.sha }}-$(date +%s)
|
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 \
|
docker build \
|
||||||
--build-arg BUILD_MODE=${{ inputs.environment == 'prod' && 'production' || 'development' }} \
|
--build-arg "BUILD_MODE=$BUILD_MODE" \
|
||||||
-t $REGISTRY/$APP:$TAG \
|
-t "$REGISTRY/$APP:$TAG" \
|
||||||
-t $REGISTRY/$APP:${{ inputs.environment }} .
|
-t "$REGISTRY/$APP:$ENV" .
|
||||||
docker push $REGISTRY/$APP:$TAG
|
docker push "$REGISTRY/$APP:$TAG"
|
||||||
docker push $REGISTRY/$APP:${{ inputs.environment }}
|
docker push "$REGISTRY/$APP:$ENV"
|
||||||
echo "IMAGE_TAG=$TAG" >> $GITHUB_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
|
- name: Setup kubectl
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
curl -sLO "https://dl.k8s.io/release/v1.30.0/bin/linux/amd64/kubectl"
|
curl -sLO "https://dl.k8s.io/release/v1.30.0/bin/linux/amd64/kubectl"
|
||||||
chmod +x kubectl
|
chmod +x kubectl
|
||||||
mv kubectl /usr/local/bin/
|
mv kubectl /usr/local/bin/
|
||||||
@@ -49,39 +111,62 @@ jobs:
|
|||||||
|
|
||||||
- name: Create/Update imagePullSecret
|
- name: Create/Update imagePullSecret
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
kubectl --kubeconfig=/tmp/kubeconfig create secret docker-registry regcred \
|
kubectl --kubeconfig=/tmp/kubeconfig create secret docker-registry regcred \
|
||||||
--docker-server=$REGISTRY \
|
--docker-server="$REGISTRY" \
|
||||||
--docker-username="${{ secrets.REGISTRY_USERNAME }}" \
|
--docker-username="${{ secrets.REGISTRY_USERNAME }}" \
|
||||||
--docker-password="${{ secrets.REGISTRY_PASSWORD }}" \
|
--docker-password="${{ secrets.REGISTRY_PASSWORD }}" \
|
||||||
-n lpt-${{ inputs.environment }} \
|
-n "lpt-${{ inputs.environment }}" \
|
||||||
--dry-run=client -o yaml | kubectl --kubeconfig=/tmp/kubeconfig apply -f -
|
--dry-run=client -o yaml | kubectl --kubeconfig=/tmp/kubeconfig apply -f -
|
||||||
|
|
||||||
- name: Deploy to K8s
|
- name: Deploy to K8s
|
||||||
run: |
|
run: |
|
||||||
kubectl --kubeconfig=/tmp/kubeconfig set image deployment/$APP $APP=$REGISTRY/$APP:$IMAGE_TAG -n lpt-${{ inputs.environment }} --record
|
set -euo pipefail
|
||||||
kubectl --kubeconfig=/tmp/kubeconfig rollout status deployment/$APP -n lpt-${{ inputs.environment }} --timeout=5m
|
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
|
- name: Debug on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
run: |
|
run: |
|
||||||
|
ENV="${{ inputs.environment }}"
|
||||||
echo "=== Deployment Status ==="
|
echo "=== Deployment Status ==="
|
||||||
kubectl --kubeconfig=/tmp/kubeconfig get deployment $APP -n lpt-${{ inputs.environment }}
|
kubectl --kubeconfig=/tmp/kubeconfig get deployment "$APP" -n "lpt-$ENV" || true
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Pod Status ==="
|
echo "=== Pod Status ==="
|
||||||
kubectl --kubeconfig=/tmp/kubeconfig get pods -n lpt-${{ inputs.environment }} -l app=$APP
|
kubectl --kubeconfig=/tmp/kubeconfig get pods -n "lpt-$ENV" -l "app=$APP" || true
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Recent Events ==="
|
echo "=== Recent Events ==="
|
||||||
kubectl --kubeconfig=/tmp/kubeconfig get events -n lpt-${{ inputs.environment }} --sort-by='.lastTimestamp' | tail -20
|
kubectl --kubeconfig=/tmp/kubeconfig get events -n "lpt-$ENV" --sort-by='.lastTimestamp' | tail -20 || true
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Pod Describe (latest) ==="
|
echo "=== Pod Describe (latest) ==="
|
||||||
POD=$(kubectl --kubeconfig=/tmp/kubeconfig get pods -n lpt-${{ inputs.environment }} -l app=$APP --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1].metadata.name}')
|
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)
|
||||||
kubectl --kubeconfig=/tmp/kubeconfig describe pod $POD -n lpt-${{ inputs.environment }} || true
|
if [ -n "${POD:-}" ]; then
|
||||||
echo ""
|
kubectl --kubeconfig=/tmp/kubeconfig describe pod "$POD" -n "lpt-$ENV" || true
|
||||||
echo "=== Pod Logs (latest) ==="
|
echo ""
|
||||||
kubectl --kubeconfig=/tmp/kubeconfig logs $POD -n lpt-${{ inputs.environment }} --tail=50 || true
|
echo "=== Pod Logs (latest) ==="
|
||||||
|
kubectl --kubeconfig=/tmp/kubeconfig logs "$POD" -n "lpt-$ENV" --tail=50 || true
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Rollback on failure
|
- name: Rollback on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
run: |
|
run: |
|
||||||
kubectl --kubeconfig=/tmp/kubeconfig rollout undo deployment/$APP -n lpt-${{ inputs.environment }}
|
set -euo pipefail
|
||||||
kubectl --kubeconfig=/tmp/kubeconfig rollout status deployment/$APP -n lpt-${{ inputs.environment }}
|
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
|
||||||
|
|||||||
+46
-74
@@ -1,100 +1,72 @@
|
|||||||
# 部署流水线问题排查进度
|
# 部署流水线问题排查进度
|
||||||
|
|
||||||
**日期**: 2026-07-19
|
**日期**: 2026-07-20(更新)
|
||||||
|
|
||||||
## 问题概述
|
## 当前部署方式
|
||||||
|
|
||||||
K8s 部署时出现 `ImagePullBackOff` 错误,容器无法拉取私有 Registry 镜像。
|
- **Jenkinsfile / Jenkinsfile-dev**:历史方案,已不再作为主路径
|
||||||
|
- **现行方案**:Gitea Actions(`.gitea/workflows/deploy.yml`)
|
||||||
|
手动 `workflow_dispatch` → 构建镜像 → 推私有 Registry → `kubectl set image` 到 K8s
|
||||||
|
|
||||||
## 根本原因
|
## 分支 ↔ 环境约定(已写入 workflow 强制校验)
|
||||||
|
|
||||||
**Docker Registry 认证问题**:
|
| 仓库 | 部署 dev (`lpt-dev`) | 部署 prod (`lpt-prod`) |
|
||||||
- Registry (`192.168.123.199:5000`) 需要认证(用户名: `admin`)
|
|------|----------------------|-------------------------|
|
||||||
- Gitea Actions workflow 使用 `secrets.REGISTRY_USERNAME` 和 `secrets.REGISTRY_PASSWORD`
|
| **lpt-fe** | 必须在 **`dev` 分支** 上 Run | 必须在 **`master`/`main`** 上 Run |
|
||||||
- **这两个 secrets 在 Gitea 项目中尚未配置**,导致:
|
| **lpt-be** | 必须在 **`dev` 分支** 上 Run | 必须在 **`master`/`main`** 上 Run |
|
||||||
1. workflow 的 `docker login` 失败
|
| **lpt-ai** | 仅有 **`main`**,从 main 部署到 dev | 从 **main** 部署到 prod |
|
||||||
2. `docker push` 虽然执行但实际未推送成功
|
|
||||||
3. K8s 拉取镜像时认证失败 → `authorization failed: no basic auth credentials`
|
|
||||||
|
|
||||||
## 已完成的修复
|
在错误分支上选择 environment 会在第一步直接失败,避免「dev 环境跑了 master 代码」。
|
||||||
|
|
||||||
### 1. Gitea Actions 配置修复(已推送)
|
### 正确操作
|
||||||
- ✅ **lpt-fe**: 修复 `github.sha` → `gitea.sha`
|
|
||||||
- ✅ **lpt-be**: 修复 `github.sha` → `gitea.sha`,kubectl 版本改为 `v1.30.0`
|
|
||||||
- ✅ **lpt-ai**: 修复 `github.sha` → `gitea.sha`,kubectl 版本改为 `v1.30.0`
|
|
||||||
|
|
||||||
### 2. K8s Deployment 修复(已推送)
|
1. Gitea 仓库页面切换到目标分支(fe/be 的 dev 或 master)
|
||||||
- ✅ 所有 Deployment YAML 添加 `imagePullSecrets: [name: regcred]`
|
2. Actions → 对应 workflow → Run workflow
|
||||||
- ✅ workflow 自动创建 `regcred` secret(使用 Gitea secrets)
|
3. 选择 `environment` = `dev` 或 `prod`(须与当前分支匹配)
|
||||||
|
|
||||||
### 3. Git 合并冲突
|
## 已修复项(2026-07-20)
|
||||||
- ⚠️ **lpt-ai** 和 **lpt-be** 的 workflow 文件存在合并冲突标记
|
|
||||||
- 需要手动解决冲突后重新提交
|
|
||||||
|
|
||||||
## 待完成任务
|
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 与最新代码
|
||||||
|
|
||||||
### 关键步骤:配置 Gitea Secrets
|
## 历史问题(部分仍可能相关)
|
||||||
|
|
||||||
需要在 Gitea 中为每个项目添加 Registry 认证信息:
|
### Registry / Secrets
|
||||||
|
|
||||||
**项目列表**:
|
若出现 `ImagePullBackOff` / `docker login` 失败,检查各仓库 Gitea Secrets:
|
||||||
- `lpt-fe`
|
|
||||||
- `lpt-be`
|
|
||||||
- `lpt-ai`
|
|
||||||
|
|
||||||
**操作步骤**(每个项目重复):
|
- `REGISTRY_USERNAME`(如 `admin`)
|
||||||
1. 进入项目页面(如 `http://192.168.123.199:3000/cat/lpt-fe`)
|
- `REGISTRY_PASSWORD`
|
||||||
2. Settings → Secrets
|
- `KUBECONFIG_B64`(kubeconfig 的 base64)
|
||||||
3. 添加两个 secrets:
|
|
||||||
- Name: `REGISTRY_USERNAME`, Value: `admin`
|
|
||||||
- Name: `REGISTRY_PASSWORD`, Value: `<htpasswd 对应的明文密码>`
|
|
||||||
|
|
||||||
**Registry 密码位置**: `/vol1/1000/registry/auth/htpasswd`(服务器 `192.168.123.199`)
|
Registry: `192.168.123.199:5000`
|
||||||
|
|
||||||
### 次要任务
|
### K8s
|
||||||
|
|
||||||
1. **解决 Git 合并冲突**
|
- 共享清单:`k8s/dev/*`、`k8s/prod/*`(按环境)
|
||||||
- 文件: `lpt-ai/.gitea/workflows/deploy.yml:34-40`
|
- 各服务仓库内 `k8s/` 仍多为 `lpt-dev` 模板;日常 CI 只 `set image`,不 `apply` 整份 YAML
|
||||||
- 文件: `lpt-be/.gitea/workflows/deploy.yml:43-48`
|
- Deployment 需有 `imagePullSecrets: [regcred]`;workflow 每次会 create/update `regcred`
|
||||||
- 清理 `<<<<<<< HEAD`, `=======`, `>>>>>>> xxx` 标记
|
|
||||||
|
|
||||||
2. **验证部署**
|
## 镜像与命名空间
|
||||||
- 配置 secrets 后触发一次提交
|
|
||||||
- 检查 workflow 日志中 `docker login` 和 `docker push` 是否成功
|
|
||||||
- 检查 K8s Pod 是否正常启动:`kubectl get pods -n lpt`
|
|
||||||
|
|
||||||
## 其他发现的问题(非紧急)
|
| environment | 镜像 tag(浮动) | 不可变 tag | namespace |
|
||||||
|
|-------------|------------------|------------|-----------|
|
||||||
|
| dev | `app:dev` | `app:<sha>-<unix>` | `lpt-dev` |
|
||||||
|
| prod | `app:prod` | `app:<sha>-<unix>` | `lpt-prod` |
|
||||||
|
|
||||||
### Jenkins 流水线(lpt-fe, lpt-be)
|
部署使用不可变 tag,避免 `imagePullPolicy` 与缓存导致未更新。
|
||||||
- 硬编码镜像标签(`0.0`),无法追溯版本
|
|
||||||
- lpt-be 健康检查时间过短(5秒),Spring Boot 启动通常需要 10-30 秒
|
|
||||||
- lpt-be Docker 网络配置可能冲突
|
|
||||||
|
|
||||||
### 流水线通用问题
|
## 验证清单
|
||||||
- 缺少镜像清理机制
|
|
||||||
- 缺少部署失败回滚机制
|
|
||||||
|
|
||||||
## 技术细节
|
1. [ ] 三仓库 Secrets 已配置
|
||||||
|
2. [ ] lpt-fe:在 **dev** 分支 Run → environment=dev 成功
|
||||||
### Registry 认证验证
|
3. [ ] lpt-fe:在 **master** 上选 environment=dev 应 **失败**(校验)
|
||||||
```bash
|
4. [ ] lpt-be:同上
|
||||||
curl -I http://192.168.123.199:5000/v2/
|
5. [ ] lpt-ai:在 **main** 上 Run → dev / prod
|
||||||
# 返回: 401 Unauthorized
|
6. [ ] `kubectl get pods -n lpt-dev` / `lpt-prod` 正常
|
||||||
# Www-Authenticate: Basic realm="Registry Realm"
|
|
||||||
```
|
|
||||||
|
|
||||||
### htpasswd 文件内容
|
|
||||||
```
|
|
||||||
admin:$2y$05$PlW.emHsl9qePGTsQPNAzecCkyNWKFO252EpjcRNV9vC8Z9AkGGZ.
|
|
||||||
```
|
|
||||||
|
|
||||||
## 下一步操作清单
|
|
||||||
|
|
||||||
1. [ ] 在 Gitea 中配置 3 个项目的 Registry secrets
|
|
||||||
2. [ ] 解决 lpt-ai 和 lpt-be 的 workflow 合并冲突
|
|
||||||
3. [ ] 提交修复并触发 workflow
|
|
||||||
4. [ ] 验证部署成功
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**备注**: 所有 K8s 配置文件修改已完成并推送到 Gitea,核心问题在于 Gitea Secrets 配置缺失。
|
**备注**: 主路径为 Gitea Actions + K8s;Jenkins 配置可保留作参考,勿与现行流程混用。
|
||||||
|
|||||||
Reference in New Issue
Block a user