From 16652ffdb07b3cbf3a764baf3ad46165ae149067 Mon Sep 17 00:00:00 2001 From: zyc <1439655764@qq.com> Date: Wed, 27 May 2026 10:19:36 +0800 Subject: [PATCH] Add Gitea CI and Kubernetes deployment --- .dockerignore | 4 + .gitea/workflows/deploy.yaml | 117 +++++++++++++++++++++++++++++ Dockerfile | 24 ++++-- k8s/cert-manager-issuer.yaml | 14 ++++ k8s/redirect-https-middleware.yaml | 8 ++ k8s/toonflow-deployment.yaml | 111 +++++++++++++++++++++++++++ k8s/toonflow-ingress.yaml | 24 ++++++ k8s/toonflow-pvc.yaml | 10 +++ scripts/docker-entrypoint.sh | 36 +++++++++ 9 files changed, 341 insertions(+), 7 deletions(-) create mode 100644 .gitea/workflows/deploy.yaml create mode 100644 k8s/cert-manager-issuer.yaml create mode 100644 k8s/redirect-https-middleware.yaml create mode 100644 k8s/toonflow-deployment.yaml create mode 100644 k8s/toonflow-ingress.yaml create mode 100644 k8s/toonflow-pvc.yaml create mode 100755 scripts/docker-entrypoint.sh diff --git a/.dockerignore b/.dockerignore index 983b9c0..0baaa1f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -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 diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml new file mode 100644 index 0000000..f8bd481 --- /dev/null +++ b/.gitea/workflows/deploy.yaml @@ -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 diff --git a/Dockerfile b/Dockerfile index 5ae2fc9..12980fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] \ No newline at end of file +ENTRYPOINT ["scripts/docker-entrypoint.sh"] +CMD ["node", "data/serve/app.js"] diff --git a/k8s/cert-manager-issuer.yaml b/k8s/cert-manager-issuer.yaml new file mode 100644 index 0000000..f0b2af8 --- /dev/null +++ b/k8s/cert-manager-issuer.yaml @@ -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 diff --git a/k8s/redirect-https-middleware.yaml b/k8s/redirect-https-middleware.yaml new file mode 100644 index 0000000..e5eedb9 --- /dev/null +++ b/k8s/redirect-https-middleware.yaml @@ -0,0 +1,8 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: redirect-https +spec: + redirectScheme: + scheme: https + permanent: true diff --git a/k8s/toonflow-deployment.yaml b/k8s/toonflow-deployment.yaml new file mode 100644 index 0000000..2d2660f --- /dev/null +++ b/k8s/toonflow-deployment.yaml @@ -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 diff --git a/k8s/toonflow-ingress.yaml b/k8s/toonflow-ingress.yaml new file mode 100644 index 0000000..4d2c0ab --- /dev/null +++ b/k8s/toonflow-ingress.yaml @@ -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 diff --git a/k8s/toonflow-pvc.yaml b/k8s/toonflow-pvc.yaml new file mode 100644 index 0000000..770ad91 --- /dev/null +++ b/k8s/toonflow-pvc.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: toonflow-data +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 50Gi diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh new file mode 100755 index 0000000..a90203b --- /dev/null +++ b/scripts/docker-entrypoint.sh @@ -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 "$@"