256 lines
11 KiB
YAML
256 lines
11 KiB
YAML
# 对齐 project0(projectBase)CI:Gitea + Docker + K8s
|
||
# 本仓为 Vite 静态宿主,构建产物为 dist/,镜像用 nginx 监听 3000
|
||
name: CI Pipeline
|
||
|
||
on:
|
||
push:
|
||
branches: [master]
|
||
workflow_dispatch:
|
||
|
||
jobs:
|
||
check-commit:
|
||
runs-on: ubuntu-latest
|
||
outputs:
|
||
should_build: ${{ steps.check.outputs.should_build }}
|
||
should_deploy: ${{ steps.check.outputs.should_deploy }}
|
||
should_deploy_only: ${{ steps.check.outputs.should_deploy_only }}
|
||
steps:
|
||
- name: Checkout Code
|
||
uses: actions/checkout@v4
|
||
with:
|
||
lfs: false
|
||
fetch-depth: 1
|
||
|
||
- name: Check commit message
|
||
id: check
|
||
run: |
|
||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||
echo "should_deploy=true" >> $GITHUB_OUTPUT
|
||
echo "should_deploy_only=false" >> $GITHUB_OUTPUT
|
||
echo "✅ 手动触发"
|
||
else
|
||
head_msg="${{ github.event.head_commit.message }}"
|
||
echo "检查提交消息: $head_msg"
|
||
|
||
if echo "$head_msg" | grep -qE '\[deploy-only\]'; then
|
||
echo "should_build=false" >> $GITHUB_OUTPUT
|
||
echo "should_deploy=true" >> $GITHUB_OUTPUT
|
||
echo "should_deploy_only=true" >> $GITHUB_OUTPUT
|
||
echo "✅ 找到 [deploy-only] 标记,只执行部署(跳过构建)"
|
||
elif echo "$head_msg" | grep -qE '\[build\]|\[deploy\]'; then
|
||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||
echo "should_deploy_only=false" >> $GITHUB_OUTPUT
|
||
echo "✅ 找到 [build] 或 [deploy] 标记"
|
||
|
||
if echo "$head_msg" | grep -qE '\[deploy\]'; then
|
||
echo "should_deploy=true" >> $GITHUB_OUTPUT
|
||
echo "✅ 找到 [deploy] 标记,将执行构建和部署"
|
||
else
|
||
echo "should_deploy=false" >> $GITHUB_OUTPUT
|
||
echo "✅ 只找到 [build] 标记,只执行构建"
|
||
fi
|
||
else
|
||
echo "should_build=false" >> $GITHUB_OUTPUT
|
||
echo "should_deploy=false" >> $GITHUB_OUTPUT
|
||
echo "should_deploy_only=false" >> $GITHUB_OUTPUT
|
||
echo "ℹ️ 提交消息不包含 [build]、[deploy] 或 [deploy-only] 标记,跳过工作流"
|
||
fi
|
||
fi
|
||
|
||
build:
|
||
needs: check-commit
|
||
if: needs.check-commit.outputs.should_build == 'true'
|
||
runs-on: ubuntu-latest
|
||
outputs:
|
||
image_ref: ${{ steps.push.outputs.image_ref }}
|
||
|
||
steps:
|
||
- name: Checkout Code
|
||
uses: actions/checkout@v4
|
||
with:
|
||
lfs: false
|
||
persist-credentials: true
|
||
token: ${{ secrets.LFS_PAT || secrets.GITHUB_TOKEN }}
|
||
|
||
- name: Force LFS Pull with Custom Auth
|
||
run: |
|
||
HOST="git.bigmonk.org"
|
||
REPO_PATH="${{ github.repository }}"
|
||
|
||
if [ -z "${{ secrets.LFS_PAT }}" ]; then
|
||
echo "❌ 错误: 未配置 LFS_PAT Secret"
|
||
exit 1
|
||
fi
|
||
|
||
GIT_TOKEN="${{ secrets.LFS_PAT }}"
|
||
git config --local --unset credential.helper || true
|
||
git config --global --unset credential.helper || true
|
||
|
||
AUTH_REMOTE="https://x-access-token:${GIT_TOKEN}@${HOST}/${REPO_PATH}.git"
|
||
AUTH_LFS_URL="https://x-access-token:${GIT_TOKEN}@${HOST}/${REPO_PATH}.git/info/lfs"
|
||
|
||
git remote set-url origin "${AUTH_REMOTE}"
|
||
git config --local lfs.url "${AUTH_LFS_URL}"
|
||
|
||
git lfs install
|
||
git lfs pull
|
||
echo "✅ LFS files pulled successfully"
|
||
|
||
- name: Setup Node.js and pnpm
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: '24'
|
||
|
||
- name: Install pnpm
|
||
run: |
|
||
corepack enable
|
||
corepack prepare [email protected] --activate
|
||
|
||
- name: Configure npm cache proxy and retries
|
||
run: |
|
||
echo "NPM_CONFIG_REGISTRY=http://172.17.0.1:4873" >> $GITHUB_ENV
|
||
echo "npm_config_registry=http://172.17.0.1:4873" >> $GITHUB_ENV
|
||
echo "PNPM_CONFIG_REGISTRY=http://172.17.0.1:4873" >> $GITHUB_ENV
|
||
pnpm config set registry http://172.17.0.1:4873
|
||
pnpm config set fetch-retries 6
|
||
pnpm config set fetch-retry-factor 2
|
||
pnpm config set fetch-retry-mintimeout 20000
|
||
pnpm config set fetch-retry-maxtimeout 120000
|
||
pnpm config set network-concurrency 8
|
||
pnpm config set child-concurrency 4
|
||
|
||
- name: Install dependencies
|
||
run: |
|
||
pnpm install --frozen-lockfile --prefer-offline \
|
||
|| pnpm install --frozen-lockfile --prefer-offline --registry=https://registry.npmjs.org
|
||
|
||
- name: Build Vite application
|
||
env:
|
||
VITE_A_ORIGIN: ${{ secrets.VITE_A_ORIGIN }}
|
||
run: |
|
||
if [ -z "${VITE_A_ORIGIN}" ]; then
|
||
echo "❌ 未配置 Secret VITE_A_ORIGIN(A 站源站,如 https://swissicapital.net)"
|
||
exit 1
|
||
fi
|
||
echo "VITE_A_ORIGIN=${VITE_A_ORIGIN}"
|
||
pnpm build
|
||
echo "✅ Build completed"
|
||
if [ -d "dist" ] && [ -f "dist/index.html" ]; then
|
||
echo "✅ dist/index.html 存在"
|
||
else
|
||
echo "❌ dist 构建产物不存在"
|
||
exit 1
|
||
fi
|
||
|
||
- name: Verify Docker access
|
||
run: docker info
|
||
|
||
- name: Build Docker image
|
||
run: |
|
||
export REGISTRY="104.225.236.253:30500"
|
||
export REPO_NAME="${{ github.event.repository.name }}"
|
||
export IMAGE_LATEST="${REGISTRY}/${REPO_NAME}:latest"
|
||
export IMAGE_SHA="${REGISTRY}/${REPO_NAME}:${GITHUB_SHA}"
|
||
docker build -t "${IMAGE_LATEST}" -t "${IMAGE_SHA}" .
|
||
echo "Image built: ${IMAGE_LATEST}"
|
||
echo "Image built: ${IMAGE_SHA}"
|
||
|
||
- name: Push to registry (SHA + latest, deploy by digest)
|
||
id: push
|
||
run: |
|
||
set -euo pipefail
|
||
export REGISTRY="104.225.236.253:30500"
|
||
export REPO_NAME="${{ github.event.repository.name }}"
|
||
export IMAGE_LATEST="${REGISTRY}/${REPO_NAME}:latest"
|
||
export IMAGE_SHA="${REGISTRY}/${REPO_NAME}:${GITHUB_SHA}"
|
||
|
||
docker push "${IMAGE_SHA}"
|
||
docker push "${IMAGE_LATEST}"
|
||
|
||
IMAGE_REF="${IMAGE_SHA}"
|
||
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "${IMAGE_SHA}" 2>/dev/null || true)
|
||
if [ -n "${DIGEST}" ]; then
|
||
IMAGE_REF="${DIGEST}"
|
||
fi
|
||
echo "image_ref=${IMAGE_REF}" >> "${GITHUB_OUTPUT}"
|
||
echo "✅ Pushed ${IMAGE_SHA} and ${IMAGE_LATEST}"
|
||
echo "✅ Rollout ref (digest preferred): ${IMAGE_REF}"
|
||
|
||
deploy:
|
||
needs: [check-commit, build]
|
||
if: needs.check-commit.outputs.should_deploy == 'true' && needs.check-commit.outputs.should_deploy_only != 'true'
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- name: Deploy to K8s
|
||
run: |
|
||
if [ -z "${{ secrets.KUBECONFIG_BASE64 }}" ]; then
|
||
echo "INFO: KUBECONFIG_BASE64 not configured. Deployment skipped."
|
||
exit 0
|
||
fi
|
||
|
||
mkdir -p ~/.kube
|
||
echo "${{ secrets.KUBECONFIG_BASE64 }}" | base64 -d > ~/.kube/config
|
||
chmod 600 ~/.kube/config
|
||
|
||
kubectl cluster-info || { echo "ERROR: Failed to connect to K8s cluster"; exit 1; }
|
||
|
||
export REPO_NAME="${{ github.event.repository.name }}"
|
||
export IMAGE_REF="${{ needs.build.outputs.image_ref }}"
|
||
export NAMESPACE="ns-${REPO_NAME}"
|
||
export DEPLOYMENT="${REPO_NAME}-deploy"
|
||
|
||
echo "Deploying ${IMAGE_REF} to ${NAMESPACE}/${DEPLOYMENT}"
|
||
kubectl patch "deployment/${DEPLOYMENT}" -n "${NAMESPACE}" --type='json' -p='[
|
||
{"op":"replace","path":"/spec/strategy/rollingUpdate/maxSurge","value":"100%"},
|
||
{"op":"replace","path":"/spec/strategy/rollingUpdate/maxUnavailable","value":0},
|
||
{"op":"replace","path":"/spec/template/spec/containers/0/image","value":"'"${IMAGE_REF}"'"},
|
||
{"op":"replace","path":"/spec/template/spec/containers/0/imagePullPolicy","value":"IfNotPresent"},
|
||
{"op":"replace","path":"/spec/template/spec/containers/0/readinessProbe/httpGet/path","value":"/api/health"},
|
||
{"op":"replace","path":"/spec/template/spec/containers/0/readinessProbe/timeoutSeconds","value":5},
|
||
{"op":"replace","path":"/spec/template/spec/containers/0/livenessProbe/httpGet/path","value":"/api/health"},
|
||
{"op":"replace","path":"/spec/template/spec/containers/0/livenessProbe/timeoutSeconds","value":5}
|
||
]'
|
||
kubectl rollout status "deployment/${DEPLOYMENT}" -n "${NAMESPACE}" --timeout=180s
|
||
echo "✅ Deployment completed successfully"
|
||
|
||
deploy-only:
|
||
needs: check-commit
|
||
if: needs.check-commit.outputs.should_deploy_only == 'true'
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- name: Deploy to K8s (without build)
|
||
run: |
|
||
if [ -z "${{ secrets.KUBECONFIG_BASE64 }}" ]; then
|
||
echo "INFO: KUBECONFIG_BASE64 not configured. Deployment skipped."
|
||
exit 0
|
||
fi
|
||
|
||
mkdir -p ~/.kube
|
||
echo "${{ secrets.KUBECONFIG_BASE64 }}" | base64 -d > ~/.kube/config
|
||
chmod 600 ~/.kube/config
|
||
|
||
kubectl cluster-info || { echo "ERROR: Failed to connect to K8s cluster"; exit 1; }
|
||
|
||
export REGISTRY="104.225.236.253:30500"
|
||
export REPO_NAME="${{ github.event.repository.name }}"
|
||
export IMAGE_LATEST="${REGISTRY}/${REPO_NAME}:latest"
|
||
export NAMESPACE="ns-${REPO_NAME}"
|
||
export DEPLOYMENT="${REPO_NAME}-deploy"
|
||
|
||
echo "Deploying existing image ${IMAGE_LATEST} to ${NAMESPACE}/${DEPLOYMENT}"
|
||
kubectl patch "deployment/${DEPLOYMENT}" -n "${NAMESPACE}" --type='json' -p='[
|
||
{"op":"replace","path":"/spec/strategy/rollingUpdate/maxSurge","value":"100%"},
|
||
{"op":"replace","path":"/spec/strategy/rollingUpdate/maxUnavailable","value":0},
|
||
{"op":"replace","path":"/spec/template/spec/containers/0/image","value":"'"${IMAGE_LATEST}"'"},
|
||
{"op":"replace","path":"/spec/template/spec/containers/0/imagePullPolicy","value":"Always"},
|
||
{"op":"replace","path":"/spec/template/spec/containers/0/readinessProbe/httpGet/path","value":"/api/health"},
|
||
{"op":"replace","path":"/spec/template/spec/containers/0/readinessProbe/timeoutSeconds","value":5},
|
||
{"op":"replace","path":"/spec/template/spec/containers/0/livenessProbe/httpGet/path","value":"/api/health"},
|
||
{"op":"replace","path":"/spec/template/spec/containers/0/livenessProbe/timeoutSeconds","value":5}
|
||
]'
|
||
kubectl rollout status "deployment/${DEPLOYMENT}" -n "${NAMESPACE}" --timeout=180s
|
||
echo "✅ Deployment completed (using existing image)"
|