Compare commits

...
6 Commits
Author SHA1 Message Date
Azmat@qq.com 2a4743db09 ci: retry after label fix
Deploy dev / deploy (push) Successful in 1s
2026-09-23 15:49:25 +08:00
Azmat@qq.com 10c4aa4437 ci: retry deploy after runner workdir fix
Deploy dev / deploy (push) Successful in 11s
2026-09-23 15:47:27 +08:00
Azmat@qq.com b821e334f6 ci: trigger deploy
Deploy dev / deploy (push) Failing after 0s
2026-09-23 15:45:02 +08:00
Azmat@qq.com a477177bab ci: Gitea Actions deploy via self-hosted runner
Deploy dev / deploy (push) Failing after 0s
2026-09-23 15:41:46 +08:00
Azmat@qq.com 806d6bf62a chore: webhook deploy smoke test 2026-09-23 15:35:16 +08:00
Azmat@qq.com a6a3d88624 chore: disable old K3s Actions; add webhook deploy scripts 2026-09-23 15:16:40 +08:00
10 changed files with 605 additions and 312 deletions
+11 -285
View File
@@ -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
+294
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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 {
+66
View File
@@ -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
+51
View File
@@ -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
+63
View File
@@ -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"
+16
View File
@@ -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
+76
View File
@@ -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()