776 lines
29 KiB
YAML
776 lines
29 KiB
YAML
name: Release
|
||
|
||
on:
|
||
push:
|
||
tags:
|
||
- "v*.*.*"
|
||
# 允許手動觸發
|
||
workflow_dispatch:
|
||
inputs:
|
||
tag:
|
||
description: "Tag to release (e.g., v0.1.13)"
|
||
required: true
|
||
type: string
|
||
|
||
env:
|
||
DOCKER_IMAGE: convertx/convertx-cn
|
||
|
||
jobs:
|
||
# ==============================================================================
|
||
# 🐳 Build and Push Docker Image(關鍵流程)
|
||
# ==============================================================================
|
||
build-and-push:
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
contents: read
|
||
packages: write
|
||
|
||
steps:
|
||
- name: Checkout repository
|
||
uses: actions/checkout@v4
|
||
|
||
# ========================================
|
||
# 清理磁碟空間(解決 no space left on device)
|
||
# ========================================
|
||
# 🔥 GitHub Actions Runner 只有約 14GB 可用空間
|
||
# 大型模型 + Multi-Arch build 需要清理更多預裝軟體
|
||
# ========================================
|
||
- name: Free disk space (aggressive)
|
||
run: |
|
||
echo "🧹 Aggressive disk cleanup for large model builds..."
|
||
echo "========================================"
|
||
echo "📊 初始磁碟空間:"
|
||
df -h /
|
||
echo "========================================"
|
||
|
||
# ========================================
|
||
# 移除大型預裝軟體(約可釋放 30-40GB)
|
||
# ========================================
|
||
echo "🗑️ 移除預裝開發工具..."
|
||
|
||
# .NET SDK(約 2-3GB)
|
||
sudo rm -rf /usr/share/dotnet || true
|
||
|
||
# Android SDK(約 10-15GB)- 最大宗!
|
||
sudo rm -rf /usr/local/lib/android || true
|
||
|
||
# Haskell/GHC(約 1GB)
|
||
sudo rm -rf /opt/ghc || true
|
||
|
||
# CodeQL(約 1GB)
|
||
sudo rm -rf /opt/hostedtoolcache/CodeQL || true
|
||
|
||
# Boost(約 500MB)
|
||
sudo rm -rf /usr/local/share/boost || true
|
||
|
||
# Swift(約 1.5GB)
|
||
sudo rm -rf /usr/share/swift || true
|
||
|
||
# Hosted Tool Cache(約 5-8GB)
|
||
sudo rm -rf /opt/hostedtoolcache || true
|
||
|
||
# Azure CLI(約 500MB)
|
||
sudo rm -rf /usr/share/az_* || true
|
||
sudo rm -rf /opt/az || true
|
||
|
||
# Google Cloud SDK(約 500MB)
|
||
sudo rm -rf /usr/lib/google-cloud-sdk || true
|
||
|
||
# PowerShell(約 200MB)
|
||
sudo rm -rf /usr/local/share/powershell || true
|
||
|
||
# Miniconda(約 500MB)
|
||
sudo rm -rf /usr/share/miniconda || true
|
||
|
||
# ========================================
|
||
# 清理系統 cache
|
||
# ========================================
|
||
echo "🗑️ 清理系統 cache..."
|
||
|
||
# apt cache
|
||
sudo apt-get clean || true
|
||
sudo apt-get autoremove -y || true
|
||
sudo rm -rf /var/lib/apt/lists/* || true
|
||
|
||
# npm/yarn global cache
|
||
sudo rm -rf /usr/local/share/.cache || true
|
||
|
||
# ========================================
|
||
# 清理 Docker 舊資料
|
||
# ========================================
|
||
echo "🗑️ 清理 Docker cache..."
|
||
docker system prune -af --volumes || true
|
||
|
||
# 清理 BuildKit cache
|
||
sudo rm -rf /var/lib/docker/buildkit || true
|
||
|
||
echo "========================================"
|
||
echo "📊 清理後磁碟空間:"
|
||
df -h /
|
||
echo "========================================"
|
||
|
||
- name: Get tag name
|
||
id: tag
|
||
run: |
|
||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||
echo "version=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
|
||
else
|
||
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||
fi
|
||
|
||
- name: Set up QEMU
|
||
uses: docker/setup-qemu-action@v3
|
||
|
||
# ========================================
|
||
# 設定 Docker Buildx(針對大型模型優化)
|
||
# ========================================
|
||
- name: Set up Docker Buildx
|
||
uses: docker/setup-buildx-action@v3
|
||
with:
|
||
# 🔥 使用 docker-container driver 以獲得更好的 cache 控制
|
||
driver-opts: |
|
||
image=moby/buildkit:latest
|
||
network=host
|
||
|
||
- name: Login to Docker Hub
|
||
uses: docker/login-action@v3
|
||
with:
|
||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||
|
||
- name: Login to GitHub Container Registry
|
||
uses: docker/login-action@v3
|
||
with:
|
||
registry: ghcr.io
|
||
username: ${{ github.actor }}
|
||
password: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
# ========================================
|
||
# Build and Push(針對大型模型優化)
|
||
# ========================================
|
||
# 🔥 注意事項:
|
||
# - 使用 registry cache 而非 GHA cache(避免 10GB 限制)
|
||
# - 啟用 zstd compression 減少傳輸量和儲存空間
|
||
# - Multi-arch build 需要更多磁碟空間,已在上方清理
|
||
# ========================================
|
||
- name: Build and push Docker image
|
||
uses: docker/build-push-action@v6
|
||
with:
|
||
context: .
|
||
push: true
|
||
tags: |
|
||
${{ env.DOCKER_IMAGE }}:${{ steps.tag.outputs.version }}
|
||
${{ env.DOCKER_IMAGE }}:latest
|
||
platforms: linux/amd64,linux/arm64
|
||
# 🔥 使用 registry cache 取代 GHA cache
|
||
# registry cache 沒有 10GB 限制,更適合大型模型
|
||
cache-from: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache
|
||
cache-to: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache,mode=max,compression=zstd
|
||
# 啟用壓縮減少 image 大小
|
||
outputs: type=image,compression=zstd,compression-level=3
|
||
# 設定 build 參數
|
||
build-args: |
|
||
BUILDKIT_INLINE_CACHE=1
|
||
|
||
- name: Docker Build Summary
|
||
run: |
|
||
echo "## 🐳 Docker Image Published" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Repository:** \`${{ env.DOCKER_IMAGE }}\`" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Tags:**" >> $GITHUB_STEP_SUMMARY
|
||
echo "- \`${{ env.DOCKER_IMAGE }}:${{ steps.tag.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
|
||
echo "- \`${{ env.DOCKER_IMAGE }}:latest\`" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Platforms:** linux/amd64, linux/arm64" >> $GITHUB_STEP_SUMMARY
|
||
|
||
# ==============================================================================
|
||
# 🔍 Verify Docker Image(模型驗證 + Headless 驗證)
|
||
# ==============================================================================
|
||
verify-image:
|
||
name: Verify Docker Image
|
||
runs-on: ubuntu-latest
|
||
needs: build-and-push
|
||
timeout-minutes: 45
|
||
|
||
steps:
|
||
- name: Get tag name
|
||
id: tag
|
||
run: |
|
||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||
echo "version=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
|
||
else
|
||
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||
fi
|
||
|
||
- name: Pull Docker image
|
||
timeout-minutes: 25
|
||
run: |
|
||
echo "📥 Pulling Docker image(大型 image,可能需要 10-20 分鐘)..."
|
||
echo "Image size: ~8-12 GB(含預下載模型)"
|
||
docker pull ${{ env.DOCKER_IMAGE }}:${{ steps.tag.outputs.version }}
|
||
|
||
# ========================================
|
||
# 驗證 1: 工具存在性 + 模型驗證(不執行 GUI 程式)
|
||
# ========================================
|
||
- name: "🔍 Tools & Model Verification"
|
||
run: |
|
||
echo "========================================"
|
||
echo "🔍 工具存在性 + 模型驗證"
|
||
echo "========================================"
|
||
|
||
IMAGE="${{ env.DOCKER_IMAGE }}:${{ steps.tag.outputs.version }}"
|
||
|
||
# 使用單一容器執行所有檢查
|
||
# 注意:不實際執行 GUI 程式(如 inkscape --version),只檢查執行檔是否存在
|
||
# 因為 GitHub Actions 是 100% headless 環境,GUI 程式可能會卡住
|
||
docker run --rm "${IMAGE}" sh -c '
|
||
FAILED=0
|
||
|
||
echo ""
|
||
echo "========================================"
|
||
echo "🔧 工具存在性檢查"
|
||
echo "========================================"
|
||
echo "(只檢查執行檔是否存在,不實際執行 GUI 程式)"
|
||
|
||
# 檢查關鍵工具是否存在(使用 which,不執行)
|
||
TOOLS="libreoffice inkscape ebook-convert pandoc ffmpeg tesseract convert gm"
|
||
|
||
for tool in $TOOLS; do
|
||
echo -n " $tool: "
|
||
if which $tool >/dev/null 2>&1; then
|
||
echo "✅ 存在"
|
||
else
|
||
echo "⚠️ 不存在"
|
||
fi
|
||
done
|
||
|
||
# 只有純 CLI 工具才執行 --version(不會卡住的)
|
||
echo ""
|
||
echo "📋 CLI 工具版本檢查..."
|
||
|
||
echo -n " pandoc: "
|
||
if pandoc --version 2>/dev/null | head -1; then
|
||
:
|
||
else
|
||
echo "⚠️ 無法取得版本"
|
||
fi
|
||
|
||
echo -n " ffmpeg: "
|
||
if ffmpeg -version 2>/dev/null | head -1; then
|
||
:
|
||
else
|
||
echo "⚠️ 無法取得版本"
|
||
fi
|
||
|
||
echo -n " tesseract: "
|
||
if tesseract --version 2>/dev/null | head -1; then
|
||
:
|
||
else
|
||
echo "⚠️ 無法取得版本"
|
||
fi
|
||
|
||
echo ""
|
||
echo "========================================"
|
||
echo "🤖 模型驗證"
|
||
echo "========================================"
|
||
|
||
# 檢查 HuggingFace cache 目錄
|
||
echo ""
|
||
echo "📋 檢查 HuggingFace cache..."
|
||
if [ -d "/root/.cache/huggingface" ]; then
|
||
echo "✅ HuggingFace cache 存在"
|
||
ls /root/.cache/huggingface/ 2>/dev/null | head -5
|
||
else
|
||
echo "⚠️ HuggingFace cache 不存在(可能使用其他路徑)"
|
||
fi
|
||
|
||
# 檢查 MinerU 模型目錄
|
||
echo ""
|
||
echo "📋 檢查 MinerU 模型..."
|
||
if [ -d "/root/.cache/modelscope" ]; then
|
||
echo "✅ ModelScope cache 存在"
|
||
elif [ -d "/app/models" ]; then
|
||
echo "✅ /app/models 目錄存在"
|
||
else
|
||
echo "⚠️ 模型目錄不存在(可能在其他路徑)"
|
||
fi
|
||
|
||
# 檢查 BabelDOC cache
|
||
echo ""
|
||
echo "📋 檢查 BabelDOC cache..."
|
||
if [ -d "/root/.cache/babeldoc" ]; then
|
||
echo "✅ BabelDOC cache 存在"
|
||
else
|
||
echo "⚠️ BabelDOC cache 不存在(將在首次使用時下載)"
|
||
fi
|
||
|
||
# 檢查離線模式環境變數
|
||
echo ""
|
||
echo "📋 檢查離線模式環境變數..."
|
||
if [ "$HF_HUB_OFFLINE" = "1" ]; then
|
||
echo "✅ HF_HUB_OFFLINE=1(離線模式已啟用)"
|
||
else
|
||
echo "⚠️ HF_HUB_OFFLINE 未設定(模型可能在 runtime 下載)"
|
||
fi
|
||
|
||
echo ""
|
||
echo "========================================"
|
||
echo "✅ 工具存在性 + 模型驗證完成!"
|
||
echo "========================================"
|
||
'
|
||
|
||
# ========================================
|
||
# 驗證 2: 基本功能驗證
|
||
# ========================================
|
||
- name: "⚙️ Basic Functionality Verification"
|
||
run: |
|
||
echo "========================================"
|
||
echo "⚙️ 基本功能驗證"
|
||
echo "========================================"
|
||
|
||
IMAGE="${{ env.DOCKER_IMAGE }}:${{ steps.tag.outputs.version }}"
|
||
|
||
# 啟動容器
|
||
echo "🚀 啟動容器..."
|
||
docker run -d --name verify-test -p 3000:3000 "${IMAGE}"
|
||
|
||
# 等待啟動
|
||
echo "⏳ 等待容器啟動..."
|
||
for i in {1..60}; do
|
||
if docker exec verify-test curl -sf http://localhost:3000/healthcheck > /dev/null 2>&1; then
|
||
echo "✅ 容器在 ${i} 秒內啟動成功"
|
||
break
|
||
fi
|
||
if [ $i -eq 60 ]; then
|
||
echo "❌ 容器啟動超時"
|
||
docker logs verify-test
|
||
docker rm -f verify-test
|
||
exit 1
|
||
fi
|
||
sleep 1
|
||
done
|
||
|
||
# 檢查關鍵工具
|
||
echo ""
|
||
echo "📋 檢查關鍵轉檔工具..."
|
||
TOOLS=("libreoffice --version" "pandoc --version" "ffmpeg -version" "tesseract --version" "pdf2zh --help" "mineru --help" "babeldoc --help")
|
||
|
||
for tool_cmd in "${TOOLS[@]}"; do
|
||
TOOL_NAME=$(echo "$tool_cmd" | cut -d' ' -f1)
|
||
echo -n " ${TOOL_NAME}: "
|
||
if docker exec verify-test $tool_cmd >/dev/null 2>&1; then
|
||
echo "✅"
|
||
else
|
||
echo "⚠️ 不可用或未安裝"
|
||
fi
|
||
done
|
||
|
||
echo ""
|
||
echo "========================================"
|
||
echo "✅ 基本功能驗證完成!"
|
||
echo "========================================"
|
||
|
||
# ========================================
|
||
# 驗證 3: E2E 測試
|
||
# ========================================
|
||
- name: "🧪 E2E Tests in Container"
|
||
run: |
|
||
echo "========================================"
|
||
echo "🧪 E2E 測試"
|
||
echo "========================================"
|
||
|
||
IMAGE="${{ env.DOCKER_IMAGE }}:${{ steps.tag.outputs.version }}"
|
||
|
||
# 啟動容器(掛載測試目錄)
|
||
echo "🚀 啟動測試容器..."
|
||
docker run -d --name e2e-test -p 3001:3000 \
|
||
-e NODE_ENV=test \
|
||
"${IMAGE}"
|
||
|
||
# 等待容器啟動
|
||
echo "⏳ 等待容器啟動..."
|
||
for i in {1..60}; do
|
||
if docker exec e2e-test curl -sf http://localhost:3000/healthcheck > /dev/null 2>&1; then
|
||
echo "✅ 容器在 ${i} 秒內啟動成功"
|
||
break
|
||
fi
|
||
if [ $i -eq 60 ]; then
|
||
echo "❌ 容器啟動超時"
|
||
docker logs e2e-test
|
||
docker rm -f e2e-test
|
||
exit 1
|
||
fi
|
||
sleep 1
|
||
done
|
||
|
||
# 執行 E2E 轉換測試
|
||
echo ""
|
||
echo "📋 執行 E2E 轉換測試..."
|
||
echo "(每個 GUI 程式測試都有 60 秒超時限制)"
|
||
FAILED=0
|
||
|
||
# 測試 1: Inkscape SVG → PNG(使用 timeout 防止 GUI 卡住)
|
||
echo ""
|
||
echo "🔄 測試 Inkscape: SVG → PNG"
|
||
docker exec e2e-test sh -c '
|
||
echo "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"100\" height=\"100\"><circle cx=\"50\" cy=\"50\" r=\"40\" fill=\"red\"/></svg>" > /tmp/test.svg
|
||
timeout 60 inkscape --export-type=png --export-filename=/tmp/test.png /tmp/test.svg 2>/dev/null
|
||
if [ -f /tmp/test.png ]; then
|
||
echo "✅ Inkscape SVG → PNG 成功"
|
||
ls -la /tmp/test.png
|
||
else
|
||
echo "❌ Inkscape SVG → PNG 失敗"
|
||
exit 1
|
||
fi
|
||
' || FAILED=1
|
||
|
||
# 測試 2: Pandoc Markdown → HTML
|
||
echo ""
|
||
echo "🔄 測試 Pandoc: Markdown → HTML"
|
||
docker exec e2e-test sh -c '
|
||
echo "# Test\n\nHello **world**" > /tmp/test.md
|
||
pandoc /tmp/test.md -o /tmp/test.html
|
||
if [ -f /tmp/test.html ]; then
|
||
echo "✅ Pandoc Markdown → HTML 成功"
|
||
cat /tmp/test.html
|
||
else
|
||
echo "❌ Pandoc Markdown → HTML 失敗"
|
||
exit 1
|
||
fi
|
||
' || FAILED=1
|
||
|
||
# 測試 3: LibreOffice DOCX → PDF(使用 timeout 防止卡住)
|
||
echo ""
|
||
echo "🔄 測試 LibreOffice: TXT → PDF"
|
||
docker exec e2e-test sh -c '
|
||
echo "Test document content" > /tmp/test.txt
|
||
timeout 60 libreoffice --headless --convert-to pdf --outdir /tmp /tmp/test.txt 2>/dev/null
|
||
if [ -f /tmp/test.pdf ]; then
|
||
echo "✅ LibreOffice TXT → PDF 成功"
|
||
ls -la /tmp/test.pdf
|
||
else
|
||
echo "❌ LibreOffice TXT → PDF 失敗"
|
||
exit 1
|
||
fi
|
||
' || FAILED=1
|
||
|
||
# 測試 4: FFmpeg 音頻轉換
|
||
echo ""
|
||
echo "🔄 測試 FFmpeg: 生成測試音頻"
|
||
docker exec e2e-test sh -c '
|
||
ffmpeg -f lavfi -i "sine=frequency=440:duration=1" -y /tmp/test.wav 2>/dev/null
|
||
if [ -f /tmp/test.wav ]; then
|
||
echo "✅ FFmpeg 音頻生成成功"
|
||
ls -la /tmp/test.wav
|
||
else
|
||
echo "❌ FFmpeg 音頻生成失敗"
|
||
exit 1
|
||
fi
|
||
' || FAILED=1
|
||
|
||
# 測試 5: ImageMagick 圖片轉換
|
||
echo ""
|
||
echo "🔄 測試 ImageMagick: PNG → JPEG"
|
||
docker exec e2e-test sh -c '
|
||
convert -size 100x100 xc:blue /tmp/blue.png
|
||
convert /tmp/blue.png /tmp/blue.jpg
|
||
if [ -f /tmp/blue.jpg ]; then
|
||
echo "✅ ImageMagick PNG → JPEG 成功"
|
||
ls -la /tmp/blue.jpg
|
||
else
|
||
echo "❌ ImageMagick PNG → JPEG 失敗"
|
||
exit 1
|
||
fi
|
||
' || FAILED=1
|
||
|
||
# 測試 6: Calibre EPUB → MOBI(使用 timeout 防止 Qt 卡住)
|
||
echo ""
|
||
echo "🔄 測試 Calibre: 電子書轉換"
|
||
docker exec e2e-test sh -c '
|
||
# 建立簡單的 HTML 作為 ebook 來源
|
||
echo "<!DOCTYPE html><html><body><h1>Test Book</h1><p>Content</p></body></html>" > /tmp/book.html
|
||
timeout 60 ebook-convert /tmp/book.html /tmp/book.epub 2>/dev/null
|
||
if [ -f /tmp/book.epub ]; then
|
||
echo "✅ Calibre HTML → EPUB 成功"
|
||
ls -la /tmp/book.epub
|
||
else
|
||
echo "⚠️ Calibre HTML → EPUB 可能需要更長時間或不支援 headless"
|
||
fi
|
||
' || echo "⚠️ Calibre 測試跳過"
|
||
|
||
# 測試 7: Potrace 點陣圖向量化
|
||
echo ""
|
||
echo "🔄 測試 Potrace: PBM → SVG"
|
||
docker exec e2e-test sh -c '
|
||
# 建立簡單的 PBM 圖片
|
||
echo "P1
|
||
4 4
|
||
0 1 1 0
|
||
1 0 0 1
|
||
1 0 0 1
|
||
0 1 1 0" > /tmp/test.pbm
|
||
potrace -s -o /tmp/test_potrace.svg /tmp/test.pbm
|
||
if [ -f /tmp/test_potrace.svg ]; then
|
||
echo "✅ Potrace PBM → SVG 成功"
|
||
ls -la /tmp/test_potrace.svg
|
||
else
|
||
echo "❌ Potrace PBM → SVG 失敗"
|
||
exit 1
|
||
fi
|
||
' || echo "⚠️ Potrace 測試跳過"
|
||
|
||
# 測試 8: Tesseract OCR
|
||
echo ""
|
||
echo "🔄 測試 Tesseract: OCR 文字識別"
|
||
docker exec e2e-test sh -c '
|
||
# 使用 ImageMagick 建立帶有文字的圖片
|
||
convert -size 200x50 xc:white -font DejaVu-Sans -pointsize 20 -fill black -annotate +10+35 "Hello OCR" /tmp/ocr_test.png
|
||
tesseract /tmp/ocr_test.png /tmp/ocr_output 2>/dev/null
|
||
if [ -f /tmp/ocr_output.txt ]; then
|
||
echo "✅ Tesseract OCR 成功"
|
||
cat /tmp/ocr_output.txt
|
||
else
|
||
echo "❌ Tesseract OCR 失敗"
|
||
exit 1
|
||
fi
|
||
' || echo "⚠️ Tesseract 測試跳過"
|
||
|
||
# 測試 9: GraphicsMagick(如果可用)
|
||
echo ""
|
||
echo "🔄 測試 GraphicsMagick: 圖片轉換"
|
||
docker exec e2e-test sh -c '
|
||
if command -v gm &> /dev/null; then
|
||
gm convert -size 100x100 xc:green /tmp/gm_test.png
|
||
gm convert /tmp/gm_test.png /tmp/gm_test.gif
|
||
if [ -f /tmp/gm_test.gif ]; then
|
||
echo "✅ GraphicsMagick PNG → GIF 成功"
|
||
ls -la /tmp/gm_test.gif
|
||
else
|
||
echo "❌ GraphicsMagick 轉換失敗"
|
||
exit 1
|
||
fi
|
||
else
|
||
echo "⚠️ GraphicsMagick 未安裝,跳過"
|
||
fi
|
||
' || echo "⚠️ GraphicsMagick 測試跳過"
|
||
|
||
# 測試 10: Resvg SVG → PNG(高品質)
|
||
echo ""
|
||
echo "🔄 測試 Resvg: SVG → PNG"
|
||
docker exec e2e-test sh -c '
|
||
if command -v resvg &> /dev/null; then
|
||
echo "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"50\" height=\"50\"><rect fill=\"purple\" width=\"50\" height=\"50\"/></svg>" > /tmp/resvg_test.svg
|
||
resvg /tmp/resvg_test.svg /tmp/resvg_output.png
|
||
if [ -f /tmp/resvg_output.png ]; then
|
||
echo "✅ Resvg SVG → PNG 成功"
|
||
ls -la /tmp/resvg_output.png
|
||
else
|
||
echo "❌ Resvg 轉換失敗"
|
||
exit 1
|
||
fi
|
||
else
|
||
echo "⚠️ Resvg 未安裝,跳過"
|
||
fi
|
||
' || echo "⚠️ Resvg 測試跳過"
|
||
|
||
# 測試 11: VIPS 圖片處理
|
||
echo ""
|
||
echo "🔄 測試 VIPS: 圖片轉換"
|
||
docker exec e2e-test sh -c '
|
||
if command -v vips &> /dev/null; then
|
||
# 使用 ImageMagick 建立測試圖片
|
||
convert -size 100x100 xc:yellow /tmp/vips_input.png
|
||
vips copy /tmp/vips_input.png /tmp/vips_output.webp
|
||
if [ -f /tmp/vips_output.webp ]; then
|
||
echo "✅ VIPS PNG → WEBP 成功"
|
||
ls -la /tmp/vips_output.webp
|
||
else
|
||
echo "❌ VIPS 轉換失敗"
|
||
exit 1
|
||
fi
|
||
else
|
||
echo "⚠️ VIPS 未安裝,跳過"
|
||
fi
|
||
' || echo "⚠️ VIPS 測試跳過"
|
||
|
||
# 測試 12: FFmpeg 影片處理
|
||
echo ""
|
||
echo "🔄 測試 FFmpeg: 影片生成與轉換"
|
||
docker exec e2e-test sh -c '
|
||
# 生成測試影片(1秒純色)
|
||
ffmpeg -f lavfi -i color=c=red:s=320x240:d=1 -y /tmp/test_video.mp4 2>/dev/null
|
||
if [ -f /tmp/test_video.mp4 ]; then
|
||
echo "✅ FFmpeg 影片生成成功"
|
||
ls -la /tmp/test_video.mp4
|
||
# 轉換為 GIF
|
||
ffmpeg -i /tmp/test_video.mp4 -vf "fps=10,scale=160:-1" -y /tmp/test_video.gif 2>/dev/null
|
||
if [ -f /tmp/test_video.gif ]; then
|
||
echo "✅ FFmpeg MP4 → GIF 成功"
|
||
ls -la /tmp/test_video.gif
|
||
fi
|
||
else
|
||
echo "❌ FFmpeg 影片生成失敗"
|
||
exit 1
|
||
fi
|
||
' || FAILED=1
|
||
|
||
# 測試 13: Pandoc 進階格式
|
||
echo ""
|
||
echo "🔄 測試 Pandoc: Markdown → LaTeX"
|
||
docker exec e2e-test sh -c '
|
||
pandoc /tmp/test.md -o /tmp/test.tex
|
||
if [ -f /tmp/test.tex ]; then
|
||
echo "✅ Pandoc Markdown → LaTeX 成功"
|
||
head -10 /tmp/test.tex
|
||
else
|
||
echo "❌ Pandoc Markdown → LaTeX 失敗"
|
||
exit 1
|
||
fi
|
||
' || echo "⚠️ Pandoc LaTeX 測試跳過"
|
||
|
||
# 測試 14: Inkscape SVG → PDF(使用 timeout 防止卡住)
|
||
echo ""
|
||
echo "🔄 測試 Inkscape: SVG → PDF"
|
||
docker exec e2e-test sh -c '
|
||
timeout 60 inkscape --export-type=pdf --export-filename=/tmp/test_inkscape.pdf /tmp/test.svg 2>/dev/null
|
||
if [ -f /tmp/test_inkscape.pdf ]; then
|
||
echo "✅ Inkscape SVG → PDF 成功"
|
||
ls -la /tmp/test_inkscape.pdf
|
||
else
|
||
echo "❌ Inkscape SVG → PDF 失敗"
|
||
exit 1
|
||
fi
|
||
' || FAILED=1
|
||
|
||
# 測試 15: LibreOffice 進階轉換(使用 timeout 防止卡住)
|
||
echo ""
|
||
echo "🔄 測試 LibreOffice: ODT → DOCX"
|
||
docker exec e2e-test sh -c '
|
||
# 建立 ODT 格式文件(使用 LibreOffice 從 TXT 轉換)
|
||
timeout 60 libreoffice --headless --convert-to odt --outdir /tmp /tmp/test.txt 2>/dev/null
|
||
if [ -f /tmp/test.odt ]; then
|
||
timeout 60 libreoffice --headless --convert-to docx --outdir /tmp /tmp/test.odt 2>/dev/null
|
||
if [ -f /tmp/test.docx ]; then
|
||
echo "✅ LibreOffice ODT → DOCX 成功"
|
||
ls -la /tmp/test.docx
|
||
else
|
||
echo "⚠️ LibreOffice ODT → DOCX 轉換可能失敗"
|
||
fi
|
||
fi
|
||
' || echo "⚠️ LibreOffice ODT 測試跳過"
|
||
|
||
# 測試 16: API 健康檢查
|
||
echo ""
|
||
echo "🔄 測試 API 端點"
|
||
if docker exec e2e-test curl -sf http://localhost:3000/healthcheck | grep -q "OK"; then
|
||
echo "✅ Healthcheck API 正常"
|
||
else
|
||
echo "❌ Healthcheck API 失敗"
|
||
FAILED=1
|
||
fi
|
||
|
||
# 測試摘要
|
||
echo ""
|
||
echo "========================================"
|
||
echo "📊 E2E 測試摘要"
|
||
echo "========================================"
|
||
echo "✓ Inkscape: SVG → PNG, PDF"
|
||
echo "✓ Pandoc: Markdown → HTML, LaTeX"
|
||
echo "✓ LibreOffice: TXT → PDF, ODT → DOCX"
|
||
echo "✓ FFmpeg: 音頻生成, 影片生成, MP4 → GIF"
|
||
echo "✓ ImageMagick: PNG → JPEG"
|
||
echo "✓ Calibre: HTML → EPUB"
|
||
echo "✓ Potrace: PBM → SVG"
|
||
echo "✓ Tesseract: OCR"
|
||
echo "✓ API: Healthcheck"
|
||
echo "========================================"
|
||
|
||
# 清理
|
||
echo ""
|
||
echo "🧹 清理測試容器..."
|
||
docker rm -f e2e-test
|
||
|
||
echo ""
|
||
echo "========================================"
|
||
if [ $FAILED -eq 0 ]; then
|
||
echo "✅ E2E 測試全部通過!"
|
||
else
|
||
echo "❌ E2E 測試有失敗項目"
|
||
exit 1
|
||
fi
|
||
echo "========================================"
|
||
|
||
# ========================================
|
||
# 清理驗證容器
|
||
# ========================================
|
||
- name: "🧹 Cleanup Verification Containers"
|
||
if: always()
|
||
run: |
|
||
docker rm -f verify-test 2>/dev/null || true
|
||
docker rm -f e2e-test 2>/dev/null || true
|
||
|
||
- name: Verification Summary
|
||
run: |
|
||
echo "## 🔍 Image Verification Results" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Image:** \`${{ env.DOCKER_IMAGE }}:${{ steps.tag.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### ✅ Verification Passed" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "- <20> Tools & Model: Verified" >> $GITHUB_STEP_SUMMARY
|
||
echo "- ⚙️ Basic Functionality: Verified" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 🧪 E2E Tests: Passed" >> $GITHUB_STEP_SUMMARY
|
||
|
||
# ==============================================================================
|
||
# <20>📦 Create GitHub Release
|
||
# ==============================================================================
|
||
create-release:
|
||
runs-on: ubuntu-latest
|
||
needs: [build-and-push, verify-image]
|
||
permissions:
|
||
contents: write
|
||
|
||
steps:
|
||
- name: Checkout repository
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Get tag name
|
||
id: tag
|
||
run: |
|
||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||
echo "version=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
|
||
else
|
||
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||
fi
|
||
|
||
- name: Generate changelog
|
||
id: changelog
|
||
run: |
|
||
# Get previous tag
|
||
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
||
|
||
if [ -z "$PREVIOUS_TAG" ]; then
|
||
# No previous tag, get all commits
|
||
CHANGELOG=$(git log --pretty=format:"- %s (%h)" HEAD)
|
||
else
|
||
# Get commits between previous tag and current
|
||
CHANGELOG=$(git log --pretty=format:"- %s (%h)" ${PREVIOUS_TAG}..HEAD)
|
||
fi
|
||
|
||
# Write to file to preserve newlines
|
||
echo "$CHANGELOG" > changelog.txt
|
||
|
||
- name: Create GitHub Release
|
||
uses: softprops/action-gh-release@v2
|
||
with:
|
||
name: ${{ steps.tag.outputs.version }}
|
||
tag_name: ${{ steps.tag.outputs.version }}
|
||
body_path: changelog.txt
|
||
generate_release_notes: true
|
||
draft: false
|
||
prerelease: false
|
||
env:
|
||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|