Add Gitea CI and Kubernetes deployment
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled

This commit is contained in:
zyc 2026-05-27 10:19:36 +08:00
parent 122d2aa431
commit 16652ffdb0
9 changed files with 341 additions and 7 deletions

View File

@ -5,6 +5,7 @@ logs
uploads
.git
.gitignore
.DS_Store
*.md
LICENSE
NOTICES.txt
@ -14,3 +15,6 @@ env
docs
*.log
.env*
data/db2.sqlite
data/logs
data/oss

View File

@ -0,0 +1,117 @@
name: Build and Deploy
on:
push:
branches:
- airlabs
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
run: |
git clone --depth=1 --branch=${{ github.ref_name }} https://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)
echo "IMAGE_TAG=internal-${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=internal" >> $GITHUB_ENV
echo "DEPLOY_ENV=internal" >> $GITHUB_ENV
echo "DOMAIN_APP=videoflow.airlabs.art" >> $GITHUB_ENV
- name: Validate deploy domain
run: |
if [ "$DOMAIN_APP" = "toonflow.example.com" ]; then
echo "DOMAIN_APP is still toonflow.example.com. Replace it with the real domain before deploying."
exit 1
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 ToonFlow
id: build_toonflow
run: |
set -o pipefail
for attempt in 1 2 3; do
echo "Build ToonFlow attempt $attempt/3..."
DOCKER_BUILDKIT=0 docker build \
--tag ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/toonflow:${{ env.IMAGE_TAG }} \
--tag ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/toonflow:latest \
. 2>&1 | tee /tmp/build.log && break
echo "Attempt $attempt failed, retrying in 10s..." && sleep 10
done
for attempt in 1 2 3; do
docker push ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/toonflow:${{ env.IMAGE_TAG }} && \
docker push ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/toonflow:latest && break
echo "Push attempt $attempt failed, retrying in 10s..." && sleep 10
done
- name: Setup Kubectl
run: |
if [ -f /usr/local/bin/kubectl ]; then
echo "Using mounted kubectl"
elif command -v kubectl &>/dev/null; then
echo "kubectl already in PATH"
else
echo "Downloading kubectl..."
curl -sLO "https://dl.k8s.io/release/v1.28.0/bin/linux/amd64/kubectl"
chmod +x kubectl && mv kubectl /usr/local/bin/kubectl
fi
kubectl version --client
- name: Set kubeconfig
run: |
mkdir -p $HOME/.kube
printf '%s\n' '${{ secrets.VOLCANO_INTERNAL_KUBE_CONFIG }}' > $HOME/.kube/config
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"
kubectl cluster-info 2>&1 | head -3 || true
- name: Deploy to K3s
id: deploy
run: |
echo "Environment: ${{ env.DEPLOY_ENV }}"
CR_IMAGE="${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}"
sed -i "s|\${CI_REGISTRY_IMAGE}/toonflow:latest|${CR_IMAGE}/toonflow:${{ env.IMAGE_TAG }}|g" k8s/toonflow-deployment.yaml
sed -i "s|toonflow.example.com|${{ env.DOMAIN_APP }}|g" k8s/toonflow-deployment.yaml k8s/toonflow-ingress.yaml
for attempt in 1 2 3; do
echo "Deploy attempt $attempt/3..."
{
kubectl 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 apply -f -
kubectl apply -f k8s/cert-manager-issuer.yaml
kubectl apply -f k8s/redirect-https-middleware.yaml
kubectl apply -f k8s/toonflow-pvc.yaml
kubectl apply -f k8s/toonflow-deployment.yaml
kubectl apply -f k8s/toonflow-ingress.yaml
kubectl rollout restart deployment/toonflow
kubectl rollout status deployment/toonflow --timeout=300s
} 2>&1 | tee /tmp/deploy.log && break
echo "Attempt $attempt failed, retrying in 10s..."
sleep 10
done
- name: Docker Cleanup
if: always()
run: |
docker container prune -f
docker image prune -f
echo "Disk usage:"
df -h / | tail -1

View File

@ -2,21 +2,31 @@ FROM node:24-bookworm-slim
WORKDIR /app
RUN npm config set registry https://registry.npmmirror.com/ && \
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates python3 make g++ && \
rm -rf /var/lib/apt/lists/* && \
npm config set registry https://registry.npmmirror.com/ && \
yarn config set registry https://registry.npmmirror.com/
# Copy the repository contents into the image and install all dependencies
COPY . .
COPY package.json yarn.lock ./
# The container only runs the backend dev server, so strip Electron-only
# packages before installing to avoid downloading desktop binaries.
# The container runs the web/API service only, so strip Electron-only packages
# before installing to avoid downloading desktop binaries in CI.
RUN node -e "const fs=require('fs');const pkg=JSON.parse(fs.readFileSync('package.json','utf8'));for(const section of ['dependencies','devDependencies']){if(!pkg[section]) continue;for(const name of ['custom-electron-titlebar','electron','electron-builder','electron-rebuild','electronmon']) delete pkg[section][name];}fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)+'\n');" && \
yarn install --frozen-lockfile && \
yarn cache clean
ENV NODE_ENV=dev
COPY . .
RUN yarn build && \
mkdir -p /opt/toonflow-data && \
cp -a data/. /opt/toonflow-data/ && \
chmod +x scripts/docker-entrypoint.sh
ENV NODE_ENV=prod
ENV PORT=10588
EXPOSE 10588
CMD ["yarn", "dev"]
ENTRYPOINT ["scripts/docker-entrypoint.sh"]
CMD ["node", "data/serve/app.js"]

View File

@ -0,0 +1,14 @@
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: airlabsv001@gmail.com
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
class: traefik

View File

@ -0,0 +1,8 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-https
spec:
redirectScheme:
scheme: https
permanent: true

View File

@ -0,0 +1,111 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: toonflow
labels:
app: toonflow
spec:
replicas: 1
selector:
matchLabels:
app: toonflow
template:
metadata:
labels:
app: toonflow
spec:
imagePullSecrets:
- name: cr-pull-secret
initContainers:
- name: init-toonflow-data
image: ${CI_REGISTRY_IMAGE}/toonflow:latest
imagePullPolicy: Always
command:
- /bin/sh
- -c
- |
set -eu
mkdir -p /persist/assets /persist/oss /persist/logs /persist/modelPrompt /persist/skills /persist/vendor
[ -f /persist/db2.sqlite ] || : > /persist/db2.sqlite
for dir in assets modelPrompt skills vendor; do
if [ -d "/app/data/$dir" ] && [ -z "$(find "/persist/$dir" -mindepth 1 -maxdepth 1 2>/dev/null)" ]; then
cp -a "/app/data/$dir/." "/persist/$dir/"
fi
done
volumeMounts:
- name: toonflow-data
mountPath: /persist
containers:
- name: toonflow
image: ${CI_REGISTRY_IMAGE}/toonflow:latest
imagePullPolicy: Always
ports:
- containerPort: 10588
env:
- name: NODE_ENV
value: "prod"
- name: PORT
value: "10588"
- name: ossURL
value: "https://videoflow.airlabs.art"
volumeMounts:
- name: toonflow-data
mountPath: /app/data/db2.sqlite
subPath: db2.sqlite
- name: toonflow-data
mountPath: /app/data/oss
subPath: oss
- name: toonflow-data
mountPath: /app/data/assets
subPath: assets
- name: toonflow-data
mountPath: /app/data/logs
subPath: logs
- name: toonflow-data
mountPath: /app/data/modelPrompt
subPath: modelPrompt
- name: toonflow-data
mountPath: /app/data/skills
subPath: skills
- name: toonflow-data
mountPath: /app/data/vendor
subPath: vendor
livenessProbe:
httpGet:
path: /favicon.ico
port: 10588
initialDelaySeconds: 30
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 4
readinessProbe:
httpGet:
path: /favicon.ico
port: 10588
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "4Gi"
cpu: "2"
volumes:
- name: toonflow-data
persistentVolumeClaim:
claimName: toonflow-data
---
apiVersion: v1
kind: Service
metadata:
name: toonflow
spec:
selector:
app: toonflow
ports:
- protocol: TCP
port: 10588
targetPort: 10588

24
k8s/toonflow-ingress.yaml Normal file
View File

@ -0,0 +1,24 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: toonflow-ingress
annotations:
kubernetes.io/ingress.class: "traefik"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
traefik.ingress.kubernetes.io/router.middlewares: "default-redirect-https@kubernetescrd"
spec:
tls:
- hosts:
- videoflow.airlabs.art
secretName: toonflow-tls
rules:
- host: videoflow.airlabs.art
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: toonflow
port:
number: 10588

10
k8s/toonflow-pvc.yaml Normal file
View File

@ -0,0 +1,10 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: toonflow-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi

36
scripts/docker-entrypoint.sh Executable file
View File

@ -0,0 +1,36 @@
#!/bin/sh
set -eu
DATA_DIR="${TOONFLOW_DATA_DIR:-/app/data}"
SEED_DIR="/opt/toonflow-data"
mkdir -p "$DATA_DIR"
seed_path() {
name="$1"
if [ -e "$SEED_DIR/$name" ] && [ ! -e "$DATA_DIR/$name" ]; then
cp -a "$SEED_DIR/$name" "$DATA_DIR/$name"
fi
}
seed_dir_if_empty() {
name="$1"
if [ -d "$SEED_DIR/$name" ]; then
mkdir -p "$DATA_DIR/$name"
if [ -z "$(find "$DATA_DIR/$name" -mindepth 1 -maxdepth 1 2>/dev/null)" ]; then
cp -a "$SEED_DIR/$name/." "$DATA_DIR/$name/"
fi
fi
}
for path in assets models serve web version.txt; do
seed_path "$path"
done
for path in modelPrompt skills vendor; do
seed_dir_if_empty "$path"
done
mkdir -p "$DATA_DIR/oss" "$DATA_DIR/logs"
exec "$@"