Compare commits
6
Commits
b1ef04ee30
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a4743db09 | ||
|
|
10c4aa4437 | ||
|
|
b821e334f6 | ||
|
|
a477177bab | ||
|
|
806d6bf62a | ||
|
|
a6a3d88624 |
+11
-285
@@ -1,294 +1,20 @@
|
||||
name: Build and Deploy
|
||||
# push → Gitea Actions(本机 Act Runner)→ docker compose 部署
|
||||
name: Deploy dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
deploy:
|
||||
# 与服务器 act_runner 注册的 label 一致(host = 直接在宿主机跑,能用本机 docker)
|
||||
runs-on: yingqing
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Deploy on server
|
||||
run: |
|
||||
git clone --depth=1 --branch=${{ github.ref_name }} https://x:${{ secrets.GITHUB_TOKEN }}@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
|
||||
echo "DOMAIN_CORE=airshelf-web.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
|
||||
echo "DOMAIN_CORE=airshelf-web.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: Build and Push Core API (Django)
|
||||
id: build_core_api
|
||||
run: |
|
||||
set -o pipefail
|
||||
ok=0
|
||||
for attempt in 1 2 3; do
|
||||
echo "Build core-api attempt $attempt/3..."
|
||||
DOCKER_BUILDKIT=0 docker build \
|
||||
--tag ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/airshelf-core-api:${{ env.IMAGE_TAG }} \
|
||||
--tag ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/airshelf-core-api:latest \
|
||||
"./core/backend" 2>&1 | tee /tmp/build-core-api.log && { ok=1; break; }
|
||||
echo "Attempt $attempt failed, retrying in 10s..." && sleep 10
|
||||
done
|
||||
[ $ok -eq 1 ] || { echo "ERROR: core-api 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-core-api:${{ env.IMAGE_TAG }} && \
|
||||
docker push ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/airshelf-core-api:latest && { ok=1; break; }
|
||||
echo "Push attempt $attempt failed, retrying in 10s..." && sleep 10
|
||||
done
|
||||
[ $ok -eq 1 ] || { echo "ERROR: core-api push failed after 3 attempts"; exit 1; }
|
||||
|
||||
- name: Build and Push Core Web (React/Vite)
|
||||
id: build_core_web
|
||||
run: |
|
||||
set -o pipefail
|
||||
ok=0
|
||||
for attempt in 1 2 3; do
|
||||
echo "Build core-web attempt $attempt/3..."
|
||||
DOCKER_BUILDKIT=0 docker build \
|
||||
--tag ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/airshelf-core-web:${{ env.IMAGE_TAG }} \
|
||||
--tag ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/airshelf-core-web:latest \
|
||||
"./core/frontend" 2>&1 | tee /tmp/build-core-web.log && { ok=1; break; }
|
||||
echo "Attempt $attempt failed, retrying in 10s..." && sleep 10
|
||||
done
|
||||
[ $ok -eq 1 ] || { echo "ERROR: core-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-core-web:${{ env.IMAGE_TAG }} && \
|
||||
docker push ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/airshelf-core-web:latest && { ok=1; break; }
|
||||
echo "Push attempt $attempt failed, retrying in 10s..." && sleep 10
|
||||
done
|
||||
[ $ok -eq 1 ] || { echo "ERROR: core-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 (PRD design site)
|
||||
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
|
||||
|
||||
# ===== Core (real app) image + domain substitution =====
|
||||
sed -i "s|\${CI_REGISTRY_IMAGE}/airshelf-core-api:latest|${CR_IMAGE}/airshelf-core-api:${{ env.IMAGE_TAG }}|g" k8s/core/api-deployment.yaml k8s/core/worker-deployment.yaml k8s/core/worker-quick-deployment.yaml
|
||||
sed -i "s|\${CI_REGISTRY_IMAGE}/airshelf-core-web:latest|${CR_IMAGE}/airshelf-core-web:${{ env.IMAGE_TAG }}|g" k8s/core/web-deployment.yaml
|
||||
sed -i "s|airshelf-web.airlabs.art|${{ env.DOMAIN_CORE }}|g" k8s/core/ingress.yaml
|
||||
|
||||
# ===== Build core env file: core/backend/.env + production overrides =====
|
||||
# Source of truth is core/backend/.env (committed; real MySQL + managed Redis + TOS + ARK).
|
||||
# Override only the env-specific bits; DB_BIND_ADDRESS is dropped (dev LAN IP
|
||||
# has no NIC in-cluster), settings -> production, hosts/CSRF/CORS -> the domain.
|
||||
grep -vE '^\s*(#|$)' core/backend/.env \
|
||||
| grep -vE '^(DJANGO_SETTINGS_MODULE|DJANGO_DEBUG|DB_BIND_ADDRESS|DJANGO_ALLOWED_HOSTS|DJANGO_CSRF_TRUSTED_ORIGINS|CORS_ALLOWED_ORIGINS)=' \
|
||||
> /tmp/core.env
|
||||
{
|
||||
echo "DJANGO_SETTINGS_MODULE=airshelf.settings.production"
|
||||
echo "DJANGO_DEBUG=false"
|
||||
echo "DJANGO_ALLOWED_HOSTS=airshelf-web.airlabs.art,${{ env.DOMAIN_CORE }},localhost,127.0.0.1"
|
||||
echo "DJANGO_CSRF_TRUSTED_ORIGINS=https://airshelf-web.airlabs.art,https://${{ env.DOMAIN_CORE }}"
|
||||
echo "CORS_ALLOWED_ORIGINS=https://airshelf-web.airlabs.art,https://${{ env.DOMAIN_CORE }}"
|
||||
} >> /tmp/core.env
|
||||
|
||||
# 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 -
|
||||
|
||||
# Core backend env secret (real MySQL / managed Redis / TOS / ARK)
|
||||
kubectl $KUBECTL_TIMEOUT create secret generic airshelf-core-env \
|
||||
--from-env-file=/tmp/core.env \
|
||||
--dry-run=client -o yaml | kubectl $KUBECTL_TIMEOUT apply -f -
|
||||
|
||||
# Apply manifests — shared infra
|
||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/cert-manager-issuer.yaml
|
||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/redirect-https-middleware.yaml
|
||||
|
||||
# PRD design site
|
||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/web-deployment.yaml
|
||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/ingress.yaml
|
||||
|
||||
# Core real app (api + celery worker + web + ingress)
|
||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/core/api-deployment.yaml
|
||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/core/worker-deployment.yaml
|
||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/core/worker-quick-deployment.yaml
|
||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/core/web-deployment.yaml
|
||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/core/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
|
||||
kubectl $KUBECTL_TIMEOUT rollout restart deployment/airshelf-core-api
|
||||
kubectl $KUBECTL_TIMEOUT rollout restart deployment/airshelf-core-worker
|
||||
kubectl $KUBECTL_TIMEOUT rollout restart deployment/airshelf-core-worker-quick
|
||||
kubectl $KUBECTL_TIMEOUT rollout restart deployment/airshelf-core-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.build_core_api.outcome }}" == "failure" ]]; then
|
||||
FAILED_STEP="build"
|
||||
if [ -f /tmp/build-core-api.log ]; then
|
||||
BUILD_LOG=$(tail -50 /tmp/build-core-api.log | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g')
|
||||
fi
|
||||
elif [[ "${{ steps.build_core_web.outcome }}" == "failure" ]]; then
|
||||
FAILED_STEP="build"
|
||||
if [ -f /tmp/build-core-web.log ]; then
|
||||
BUILD_LOG=$(tail -50 /tmp/build-core-web.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
|
||||
set -euo pipefail
|
||||
test -x /opt/AirShelf/scripts/deploy-dev.sh
|
||||
/opt/AirShelf/scripts/deploy-dev.sh
|
||||
echo "deploy finished"
|
||||
cd /opt/AirShelf && git log -1 --oneline && docker compose ps
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
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://x:${{ secrets.GITHUB_TOKEN }}@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
|
||||
echo "DOMAIN_CORE=airshelf-web.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
|
||||
echo "DOMAIN_CORE=airshelf-web.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: Build and Push Core API (Django)
|
||||
id: build_core_api
|
||||
run: |
|
||||
set -o pipefail
|
||||
ok=0
|
||||
for attempt in 1 2 3; do
|
||||
echo "Build core-api attempt $attempt/3..."
|
||||
DOCKER_BUILDKIT=0 docker build \
|
||||
--tag ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/airshelf-core-api:${{ env.IMAGE_TAG }} \
|
||||
--tag ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/airshelf-core-api:latest \
|
||||
"./core/backend" 2>&1 | tee /tmp/build-core-api.log && { ok=1; break; }
|
||||
echo "Attempt $attempt failed, retrying in 10s..." && sleep 10
|
||||
done
|
||||
[ $ok -eq 1 ] || { echo "ERROR: core-api 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-core-api:${{ env.IMAGE_TAG }} && \
|
||||
docker push ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/airshelf-core-api:latest && { ok=1; break; }
|
||||
echo "Push attempt $attempt failed, retrying in 10s..." && sleep 10
|
||||
done
|
||||
[ $ok -eq 1 ] || { echo "ERROR: core-api push failed after 3 attempts"; exit 1; }
|
||||
|
||||
- name: Build and Push Core Web (React/Vite)
|
||||
id: build_core_web
|
||||
run: |
|
||||
set -o pipefail
|
||||
ok=0
|
||||
for attempt in 1 2 3; do
|
||||
echo "Build core-web attempt $attempt/3..."
|
||||
DOCKER_BUILDKIT=0 docker build \
|
||||
--tag ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/airshelf-core-web:${{ env.IMAGE_TAG }} \
|
||||
--tag ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/airshelf-core-web:latest \
|
||||
"./core/frontend" 2>&1 | tee /tmp/build-core-web.log && { ok=1; break; }
|
||||
echo "Attempt $attempt failed, retrying in 10s..." && sleep 10
|
||||
done
|
||||
[ $ok -eq 1 ] || { echo "ERROR: core-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-core-web:${{ env.IMAGE_TAG }} && \
|
||||
docker push ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/airshelf-core-web:latest && { ok=1; break; }
|
||||
echo "Push attempt $attempt failed, retrying in 10s..." && sleep 10
|
||||
done
|
||||
[ $ok -eq 1 ] || { echo "ERROR: core-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 (PRD design site)
|
||||
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
|
||||
|
||||
# ===== Core (real app) image + domain substitution =====
|
||||
sed -i "s|\${CI_REGISTRY_IMAGE}/airshelf-core-api:latest|${CR_IMAGE}/airshelf-core-api:${{ env.IMAGE_TAG }}|g" k8s/core/api-deployment.yaml k8s/core/worker-deployment.yaml k8s/core/worker-quick-deployment.yaml
|
||||
sed -i "s|\${CI_REGISTRY_IMAGE}/airshelf-core-web:latest|${CR_IMAGE}/airshelf-core-web:${{ env.IMAGE_TAG }}|g" k8s/core/web-deployment.yaml
|
||||
sed -i "s|airshelf-web.airlabs.art|${{ env.DOMAIN_CORE }}|g" k8s/core/ingress.yaml
|
||||
|
||||
# ===== Build core env file: core/backend/.env + production overrides =====
|
||||
# Source of truth is core/backend/.env (committed; real MySQL + managed Redis + TOS + ARK).
|
||||
# Override only the env-specific bits; DB_BIND_ADDRESS is dropped (dev LAN IP
|
||||
# has no NIC in-cluster), settings -> production, hosts/CSRF/CORS -> the domain.
|
||||
grep -vE '^\s*(#|$)' core/backend/.env \
|
||||
| grep -vE '^(DJANGO_SETTINGS_MODULE|DJANGO_DEBUG|DB_BIND_ADDRESS|DJANGO_ALLOWED_HOSTS|DJANGO_CSRF_TRUSTED_ORIGINS|CORS_ALLOWED_ORIGINS)=' \
|
||||
> /tmp/core.env
|
||||
{
|
||||
echo "DJANGO_SETTINGS_MODULE=airshelf.settings.production"
|
||||
echo "DJANGO_DEBUG=false"
|
||||
echo "DJANGO_ALLOWED_HOSTS=airshelf-web.airlabs.art,${{ env.DOMAIN_CORE }},localhost,127.0.0.1"
|
||||
echo "DJANGO_CSRF_TRUSTED_ORIGINS=https://airshelf-web.airlabs.art,https://${{ env.DOMAIN_CORE }}"
|
||||
echo "CORS_ALLOWED_ORIGINS=https://airshelf-web.airlabs.art,https://${{ env.DOMAIN_CORE }}"
|
||||
} >> /tmp/core.env
|
||||
|
||||
# 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 -
|
||||
|
||||
# Core backend env secret (real MySQL / managed Redis / TOS / ARK)
|
||||
kubectl $KUBECTL_TIMEOUT create secret generic airshelf-core-env \
|
||||
--from-env-file=/tmp/core.env \
|
||||
--dry-run=client -o yaml | kubectl $KUBECTL_TIMEOUT apply -f -
|
||||
|
||||
# Apply manifests — shared infra
|
||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/cert-manager-issuer.yaml
|
||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/redirect-https-middleware.yaml
|
||||
|
||||
# PRD design site
|
||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/web-deployment.yaml
|
||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/ingress.yaml
|
||||
|
||||
# Core real app (api + celery worker + web + ingress)
|
||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/core/api-deployment.yaml
|
||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/core/worker-deployment.yaml
|
||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/core/worker-quick-deployment.yaml
|
||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/core/web-deployment.yaml
|
||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/core/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
|
||||
kubectl $KUBECTL_TIMEOUT rollout restart deployment/airshelf-core-api
|
||||
kubectl $KUBECTL_TIMEOUT rollout restart deployment/airshelf-core-worker
|
||||
kubectl $KUBECTL_TIMEOUT rollout restart deployment/airshelf-core-worker-quick
|
||||
kubectl $KUBECTL_TIMEOUT rollout restart deployment/airshelf-core-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.build_core_api.outcome }}" == "failure" ]]; then
|
||||
FAILED_STEP="build"
|
||||
if [ -f /tmp/build-core-api.log ]; then
|
||||
BUILD_LOG=$(tail -50 /tmp/build-core-api.log | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g')
|
||||
fi
|
||||
elif [[ "${{ steps.build_core_web.outcome }}" == "failure" ]]; then
|
||||
FAILED_STEP="build"
|
||||
if [ -f /tmp/build-core-web.log ]; then
|
||||
BUILD_LOG=$(tail -50 /tmp/build-core-web.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
|
||||
+15
-15
@@ -1,20 +1,20 @@
|
||||
DJANGO_SETTINGS_MODULE=airshelf.settings.development
|
||||
DJANGO_SETTINGS_MODULE=airshelf.settings.production
|
||||
DJANGO_SECRET_KEY=S2GYXa8YC21lmnFfVwC+6cyFNhCCSoclhpylOmSAm16vKflUgQi398VQQSM+Rbit
|
||||
DJANGO_DEBUG=true
|
||||
DJANGO_ALLOWED_HOSTS=airshelf-web.airlabs.art,airshelf-web.test.airlabs.art,localhost,127.0.0.1,192.168.124.86
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS=https://airshelf-web.airlabs.art,https://airshelf-web.test.airlabs.art,https://airshelf.airlabs.art,http://localhost:3000,http://127.0.0.1:3000,http://localhost:5173,http://127.0.0.1:5173,http://192.168.124.86:5173
|
||||
CORS_ALLOWED_ORIGINS=https://airshelf-web.airlabs.art,https://airshelf-web.test.airlabs.art,https://airshelf.airlabs.art,http://localhost:3000,http://127.0.0.1:3000,http://localhost:5173,http://127.0.0.1:5173,http://192.168.124.86:5173
|
||||
DJANGO_DEBUG=false
|
||||
DJANGO_ALLOWED_HOSTS=yingqing.yueqing.online,localhost,127.0.0.1
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS=https://yingqing.yueqing.online
|
||||
CORS_ALLOWED_ORIGINS=https://yingqing.yueqing.online
|
||||
DB_ENGINE=mysql
|
||||
DB_NAME=airshelf_test
|
||||
DB_USER=airshelf_app
|
||||
DB_PASSWORD=d5020f4d41e0e4c52a371ecb913be3d1f1ab2b85
|
||||
DB_HOST=14.103.27.192
|
||||
DB_NAME=yingqing_app
|
||||
DB_USER=yingqing_app
|
||||
DB_PASSWORD=NWXa5nDABKxcS64A
|
||||
DB_HOST=8.163.33.160
|
||||
DB_PORT=3306
|
||||
# DB_BIND_ADDRESS=192.168.124.137 # local-dev blanked: upstream dev-host LAN addr, not present on this machine -> WinError 10049
|
||||
REDIS_CACHE_URL=redis://zyc:Zyc188208@redis-shzlsczo52dft8mia.redis.volces.com:6379/0
|
||||
CELERY_BROKER_URL=redis://zyc:Zyc188208@redis-shzlsczo52dft8mia.redis.volces.com:6379/1
|
||||
CELERY_RESULT_BACKEND=redis://zyc:Zyc188208@redis-shzlsczo52dft8mia.redis.volces.com:6379/2
|
||||
REDIS_LOCK_URL=redis://zyc:Zyc188208@redis-shzlsczo52dft8mia.redis.volces.com:6379/3
|
||||
REDIS_CACHE_URL=redis://:redis_WFSzWJ@yingqing-redis:6379/0
|
||||
CELERY_BROKER_URL=redis://:redis_WFSzWJ@yingqing-redis:6379/1
|
||||
CELERY_RESULT_BACKEND=redis://:redis_WFSzWJ@yingqing-redis:6379/2
|
||||
REDIS_LOCK_URL=redis://:redis_WFSzWJ@yingqing-redis:6379/3
|
||||
TOS_ENDPOINT=https://tos-s3-cn-shanghai.volces.com
|
||||
TOS_BUCKET=airshelf
|
||||
TOS_ACCESS_KEY_ID=AKLTODVhY2U1NzY1MTU3NDA4NThiYzk2ZDMyZDNjYmZhZGY
|
||||
@@ -107,6 +107,6 @@ MODEL_TRYON_PROMPT_V2_ENABLED=true
|
||||
MODEL_TRYON_PROMPT_V2_CANARY_TEAM_IDS=
|
||||
|
||||
# 全能创作 Agent:本机专用队列,避免远程 K8s worker 偷走未注册任务
|
||||
CREATION_AGENT_TASK_QUEUE=airshelf.local
|
||||
CREATION_AGENT_TASK_QUEUE=celery
|
||||
# 可选:API 线程内联跑 turn(不入 Celery)。队列方案足够时保持 false
|
||||
CREATION_AGENT_INLINE=true
|
||||
CREATION_AGENT_INLINE=false
|
||||
|
||||
+12
-11
@@ -1,27 +1,28 @@
|
||||
DJANGO_SETTINGS_MODULE=airshelf.settings.development
|
||||
DJANGO_SETTINGS_MODULE=airshelf.settings.production
|
||||
DJANGO_SECRET_KEY=change-me
|
||||
DJANGO_DEBUG=true
|
||||
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
|
||||
DJANGO_DEBUG=false
|
||||
DJANGO_ALLOWED_HOSTS=yingqing.yueqing.online,localhost,127.0.0.1
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS=https://yingqing.yueqing.online
|
||||
CORS_ALLOWED_ORIGINS=https://yingqing.yueqing.online
|
||||
|
||||
DB_ENGINE=mysql
|
||||
DB_NAME=airshelf_dev
|
||||
DB_USER=airshelf
|
||||
DB_NAME=yingqing_app
|
||||
DB_USER=yingqing_app
|
||||
DB_PASSWORD=change-me
|
||||
DB_HOST=127.0.0.1
|
||||
DB_HOST=yingqing-mysql
|
||||
DB_PORT=3306
|
||||
DB_BIND_ADDRESS=
|
||||
|
||||
REDIS_CACHE_URL=redis://127.0.0.1:6379/0
|
||||
CELERY_BROKER_URL=redis://127.0.0.1:6379/1
|
||||
CELERY_RESULT_BACKEND=redis://127.0.0.1:6379/2
|
||||
REDIS_CACHE_URL=redis://:YOUR_REDIS_PASSWORD@yingqing-redis:6379/0
|
||||
CELERY_BROKER_URL=redis://:YOUR_REDIS_PASSWORD@yingqing-redis:6379/1
|
||||
CELERY_RESULT_BACKEND=redis://:YOUR_REDIS_PASSWORD@yingqing-redis:6379/2
|
||||
|
||||
# 全能创作 Agent turn 队列。生产保持 celery(K8s 已部署该任务)。
|
||||
# 本机若与远程 worker 共用 broker,设为 airshelf.local,并让本机 worker -Q 含该队列。
|
||||
CREATION_AGENT_TASK_QUEUE=celery
|
||||
# true / AIRSHELF_CREATION_AGENT_INLINE=1:API 后台线程直接跑,不入 Celery(仅本机联调)
|
||||
CREATION_AGENT_INLINE=false
|
||||
REDIS_LOCK_URL=redis://127.0.0.1:6379/3
|
||||
REDIS_LOCK_URL=redis://:YOUR_REDIS_PASSWORD@yingqing-redis:6379/3
|
||||
|
||||
TOS_ENDPOINT=https://tos-s3-cn-shanghai.volces.com
|
||||
TOS_BUCKET=airshelf
|
||||
|
||||
@@ -933,7 +933,7 @@
|
||||
|
||||
.omni-case-card[data-mode="image"] .omni-case-visual {
|
||||
height: auto;
|
||||
aspect-ratio: 4 / 3;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.omni-card-use {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# yingqing / AirShelf — 1Panel + Docker Compose
|
||||
# MySQL/Redis 由 1Panel 安装,容器名:yingqing-mysql、yingqing-redis
|
||||
# 应用加入 1panel-network,反代到本机 :8080
|
||||
|
||||
services:
|
||||
airshelf-core-api:
|
||||
build:
|
||||
context: ./core/backend
|
||||
image: airshelf-core-api:local
|
||||
container_name: airshelf-core-api
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ./core/backend/.env
|
||||
environment:
|
||||
DJANGO_SETTINGS_MODULE: airshelf.settings.production
|
||||
# entrypoint 在 gunicorn 时跑 migrate + collectstatic
|
||||
command: ["gunicorn", "airshelf.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3", "--threads", "4", "--timeout", "300"]
|
||||
networks:
|
||||
- 1panel-network
|
||||
expose:
|
||||
- "8000"
|
||||
|
||||
airshelf-core-worker:
|
||||
image: airshelf-core-api:local
|
||||
container_name: airshelf-core-worker
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ./core/backend/.env
|
||||
environment:
|
||||
DJANGO_SETTINGS_MODULE: airshelf.settings.production
|
||||
command: ["celery", "-A", "airshelf.celery:app", "worker", "-l", "info", "-Q", "celery", "--concurrency", "4"]
|
||||
depends_on:
|
||||
- airshelf-core-api
|
||||
networks:
|
||||
- 1panel-network
|
||||
|
||||
airshelf-core-worker-quick:
|
||||
image: airshelf-core-api:local
|
||||
container_name: airshelf-core-worker-quick
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ./core/backend/.env
|
||||
environment:
|
||||
DJANGO_SETTINGS_MODULE: airshelf.settings.production
|
||||
command: ["celery", "-A", "airshelf.celery:app", "worker", "-l", "info", "-Q", "airshelf.quick", "--concurrency", "4"]
|
||||
depends_on:
|
||||
- airshelf-core-api
|
||||
networks:
|
||||
- 1panel-network
|
||||
|
||||
yingqing-web:
|
||||
build:
|
||||
context: ./core/frontend
|
||||
image: airshelf-yingqing-web:local
|
||||
container_name: yingqing-web
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- airshelf-core-api
|
||||
ports:
|
||||
- "8080:80"
|
||||
networks:
|
||||
- 1panel-network
|
||||
|
||||
networks:
|
||||
1panel-network:
|
||||
external: true
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# 部署 dev 到本机 Docker Compose(可由 Gitea Actions 或 Webhook 调用)
|
||||
set -euo pipefail
|
||||
|
||||
APP_DIR="${APP_DIR:-/opt/AirShelf}"
|
||||
BRANCH="${BRANCH:-dev}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}"
|
||||
ENV_FILE="${ENV_FILE:-core/backend/.env}"
|
||||
LOG_DIR="${LOG_DIR:-/var/log/airshelf}"
|
||||
LOCK_FILE="${LOCK_FILE:-/tmp/airshelf-deploy-dev.lock}"
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
LOG_FILE="${LOG_DIR}/deploy-dev.log"
|
||||
|
||||
exec 9>"$LOCK_FILE"
|
||||
if ! flock -n 9; then
|
||||
echo "$(date '+%F %T') another deploy is running, skip" | tee -a "$LOG_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
{
|
||||
echo "======== $(date '+%F %T') deploy start ========"
|
||||
cd "$APP_DIR"
|
||||
|
||||
if [[ ! -d .git ]]; then
|
||||
echo "ERROR: $APP_DIR is not a git repo. Clone from your Gitea first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# .env 在仓库里被跟踪,pull 会覆盖服务器生产配置;先备份再还原
|
||||
ENV_BACKUP=""
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
ENV_BACKUP="$(mktemp)"
|
||||
cp -a "$ENV_FILE" "$ENV_BACKUP"
|
||||
echo "backed up $ENV_FILE"
|
||||
fi
|
||||
|
||||
git fetch --prune origin
|
||||
git checkout "$BRANCH"
|
||||
git reset --hard "origin/$BRANCH"
|
||||
|
||||
if [[ -n "$ENV_BACKUP" ]]; then
|
||||
cp -a "$ENV_BACKUP" "$ENV_FILE"
|
||||
rm -f "$ENV_BACKUP"
|
||||
echo "restored $ENV_FILE"
|
||||
fi
|
||||
|
||||
docker compose -f "$COMPOSE_FILE" up -d --build
|
||||
|
||||
echo "======== $(date '+%F %T') deploy ok ========"
|
||||
} >>"$LOG_FILE" 2>&1
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
# 在部署机上一键安装 Gitea Act Runner(仅需执行一次)
|
||||
# 用法:
|
||||
# export GITEA_URL=https://gitea.yueqing.online
|
||||
# export RUNNER_TOKEN=从Gitea复制的注册Token
|
||||
# bash scripts/install-act-runner.sh
|
||||
set -euo pipefail
|
||||
|
||||
GITEA_URL="${GITEA_URL:?请设置 GITEA_URL,例如 https://gitea.yueqing.online}"
|
||||
RUNNER_TOKEN="${RUNNER_TOKEN:?请设置 RUNNER_TOKEN(Gitea → 仓库/站点 Actions → Runners → 创建)}"
|
||||
RUNNER_NAME="${RUNNER_NAME:-yingqing-ecs}"
|
||||
LABELS="${LABELS:-yingqing:host}"
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/act_runner}"
|
||||
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
cd "$INSTALL_DIR"
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
case "$ARCH" in
|
||||
x86_64|amd64) ARCH=amd64 ;;
|
||||
aarch64|arm64) ARCH=arm64 ;;
|
||||
*) echo "不支持的架构: $ARCH"; exit 1 ;;
|
||||
esac
|
||||
|
||||
VER="${ACT_RUNNER_VERSION:-0.2.11}"
|
||||
BIN_URL="https://gitea.com/gitea/act_runner/releases/download/v${VER}/act_runner-${VER}-linux-${ARCH}"
|
||||
|
||||
echo "下载 act_runner v${VER} ..."
|
||||
curl -fL "$BIN_URL" -o act_runner
|
||||
chmod +x act_runner
|
||||
|
||||
if [[ ! -f .runner ]]; then
|
||||
echo "注册 Runner ..."
|
||||
./act_runner register --no-interactive \
|
||||
--instance "$GITEA_URL" \
|
||||
--token "$RUNNER_TOKEN" \
|
||||
--name "$RUNNER_NAME" \
|
||||
--labels "$LABELS"
|
||||
fi
|
||||
|
||||
cat >/etc/systemd/system/act_runner.service <<UNIT
|
||||
[Unit]
|
||||
Description=Gitea Act Runner
|
||||
After=network.target docker.service
|
||||
Wants=docker.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=${INSTALL_DIR}
|
||||
ExecStart=${INSTALL_DIR}/act_runner daemon
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now act_runner
|
||||
systemctl --no-pager --full status act_runner || true
|
||||
echo
|
||||
echo "完成。在 Gitea → Actions → Runners 应看到「${RUNNER_NAME}」在线,label: ${LABELS}"
|
||||
echo "可停掉旧的 webhook(避免重复部署):systemctl disable --now airshelf-webhook"
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=AirShelf Gitea deploy webhook
|
||||
After=network.target docker.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Environment=WEBHOOK_SECRET=CHANGE_ME
|
||||
Environment=WEBHOOK_HOST=127.0.0.1
|
||||
Environment=WEBHOOK_PORT=9000
|
||||
Environment=DEPLOY_SH=/opt/AirShelf/scripts/deploy-dev.sh
|
||||
ExecStart=/usr/bin/python3 /opt/AirShelf/scripts/webhook/receiver.py
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Gitea push webhook → only refs/heads/dev → scripts/deploy-dev.sh"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
SECRET = os.environ.get("WEBHOOK_SECRET", "").encode()
|
||||
DEPLOY_SH = os.environ.get("DEPLOY_SH", "/opt/AirShelf/scripts/deploy-dev.sh")
|
||||
HOST = os.environ.get("WEBHOOK_HOST", "0.0.0.0")
|
||||
PORT = int(os.environ.get("WEBHOOK_PORT", "9000"))
|
||||
|
||||
|
||||
def verify(sig: str | None, body: bytes) -> bool:
|
||||
if not SECRET or not sig:
|
||||
return False
|
||||
digest = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
|
||||
return hmac.compare_digest(digest, sig.strip())
|
||||
|
||||
|
||||
def run_deploy() -> None:
|
||||
subprocess.run([DEPLOY_SH], check=False)
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, fmt: str, *args) -> None:
|
||||
print(f"[webhook] {self.address_string()} - {fmt % args}")
|
||||
|
||||
def do_POST(self) -> None:
|
||||
if self.path not in ("/hooks/deploy-dev", "/deploy-dev", "/"):
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
return
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
body = self.rfile.read(length)
|
||||
sig = self.headers.get("X-Gitea-Signature") or self.headers.get("X-Hub-Signature-256", "")
|
||||
if sig.startswith("sha256="):
|
||||
sig = sig[7:]
|
||||
if not verify(sig, body):
|
||||
self.send_response(401)
|
||||
self.end_headers()
|
||||
self.wfile.write(b"invalid signature")
|
||||
return
|
||||
try:
|
||||
payload = json.loads(body.decode("utf-8") or "{}")
|
||||
except json.JSONDecodeError:
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
return
|
||||
ref = payload.get("ref", "")
|
||||
if ref != "refs/heads/dev":
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(f"ignored ref={ref}".encode())
|
||||
return
|
||||
threading.Thread(target=run_deploy, daemon=True).start()
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(b"deploy-dev accepted")
|
||||
|
||||
def do_GET(self) -> None:
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(b"airshelf webhook ok")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if not SECRET:
|
||||
raise SystemExit("WEBHOOK_SECRET is required")
|
||||
print(f"listening on {HOST}:{PORT}, deploy={DEPLOY_SH}")
|
||||
HTTPServer((HOST, PORT), Handler).serve_forever()
|
||||
Reference in New Issue
Block a user