# ============================================================ # Docker Build & Push(Remote Host via Tailscale SSH) # ============================================================ # 說明:使用 Tailscale SSH 連線到私有主機執行 Docker build # 重要:此 workflow 完全不使用傳統 SSH key,僅使用 Tailscale SSH # ============================================================ name: Docker Build & Push (Remote Host via Tailscale) on: # 手動觸發,支援輸入 image tag workflow_dispatch: inputs: image_tag: description: "Docker Image Tag(例如:0.1.12.test)" required: true default: "0.1.12.test" type: string # ============================================================ # 環境變數設定 # ============================================================ env: # 遠端主機工作目錄 REMOTE_WORKDIR: /home/bioailab/miniconda3/lid/app/docker_build # 專案名稱 PROJECT_NAME: ConvertX-CN # Docker Hub 映像庫 DOCKER_IMAGE_REPO: convertx/convertx-cn # ============================================================ # 權限設定(GITHUB_TOKEN) # ============================================================ permissions: contents: write # 必要:創建 Release、上傳 assets、創建/推送 tag actions: read # 可選:讀取 workflow 資訊(用於 generate release notes) jobs: build-and-push: name: 🚀 遠端 Docker Build & Push runs-on: ubuntu-latest steps: # ======================================== # Step 1: Checkout repository # 說明:檢出程式碼(雖然 build 在遠端,但需要 workflow 完整性) # ======================================== - name: 📥 Checkout repository uses: actions/checkout@v4 # ======================================== # Step 2: 啟動 Tailscale(非互動模式) # 說明:使用 Tailscale Auth Key 加入 tailnet,啟用 SSH # 重要:必須使用 --ssh 參數才能使用 tailscale ssh # ======================================== - name: 🔗 啟動 Tailscale 連線(非互動模式) uses: tailscale/github-action@v2 with: authkey: ${{ secrets.TAILSCALE_AUTHKEY }} args: "--ssh --accept-dns=false --accept-routes=false" # 使用 ephemeral key,runner 結束後自動離開 tailnet # --ssh: 啟用 Tailscale SSH 功能 # --accept-dns=false: 不使用 Tailscale DNS # --accept-routes=false: 不接受其他節點的路由 # ======================================== # Step 3: 驗證 Tailscale 連線 # 說明:確認 Tailscale 已成功連線並顯示狀態 # ======================================== - name: ✅ 驗證 Tailscale 連線狀態 run: | echo "========================================" echo "🔍 檢查 Tailscale 連線狀態" echo "========================================" # 顯示 Tailscale 狀態 echo "" echo "📊 Tailscale 狀態:" tailscale status # 顯示本機 Tailscale IP echo "" echo "🌐 本機 Tailscale IP:" TAILSCALE_IP=$(tailscale ip -4) echo "${TAILSCALE_IP}" # 確認 SSH 功能已啟用 echo "" echo "🔐 檢查 SSH 功能狀態:" if tailscale status --json | grep -q '"SSH":true'; then echo "✅ Tailscale SSH 功能已啟用" else echo "⚠️ 正在檢查 SSH 狀態..." tailscale status --json | head -50 fi # 等待 Tailscale 完全連線 echo "" echo "⏳ 等待 Tailscale 連線穩定..." sleep 5 # 測試與目標主機的連通性 echo "" echo "📡 測試與遠端主機的連通性..." echo "目標主機: ${{ secrets.SSH_HOST }}" if tailscale ping --c 3 "${{ secrets.SSH_HOST }}"; then echo "" echo "✅ Tailscale 連線測試成功!" else echo "" echo "⚠️ Tailscale ping 失敗,但 SSH 可能仍可運作,繼續執行..." fi echo "========================================" # ======================================== # Step 4: Tailscale SSH 連線(手動網頁驗證) # 說明:首次連線需要點擊驗證 URL 完成授權 # ======================================== - name: 🔐 測試 Tailscale SSH 連線 run: | echo "========================================" echo "🔐 測試 Tailscale SSH 連線" echo "========================================" echo "使用者: ${{ secrets.SSH_USER }}" echo "主機: ${{ secrets.SSH_HOST }}" echo "" echo "⚠️ 如果出現驗證 URL,請在瀏覽器中開啟並完成驗證" echo "⏳ 驗證完成後,連線會自動繼續..." echo "" echo "========================================" # 執行 SSH 連線,輸出會包含驗證連結 # 使用 timeout 避免無限等待(10 分鐘) timeout 600 tailscale ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} \ "echo '✅ Tailscale SSH 連線成功!' && echo '主機名稱:' && hostname && echo '當前使用者:' && whoami" \ || { echo ""; echo "❌ SSH 連線失敗或超時,請確認已完成網頁驗證"; exit 1; } echo "" echo "========================================" # ======================================== # Step 5: Smoke Test(環境驗證) # 說明:快速驗證遠端環境是否正常 # ======================================== - name: 🧪 Smoke Test(環境驗證) run: | echo "========================================" echo "🧪 Smoke Test - 驗證遠端環境" echo "========================================" tailscale ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'REMOTE_EOF' set -e echo "📋 測試項目 1:進入目標目錄" echo "----------------------------------------" WORKDIR="/home/bioailab/miniconda3/lid/app/docker_build" mkdir -p "${WORKDIR}" cd "${WORKDIR}" echo "✅ 目前目錄: $(pwd)" echo "" echo "📋 測試項目 2:Git 版本" echo "----------------------------------------" git --version echo "✅ Git 可用" echo "" echo "📋 測試項目 3:Docker 版本" echo "----------------------------------------" docker version --format '{{.Server.Version}}' || docker --version echo "✅ Docker 可用" echo "" echo "📋 測試項目 4:Docker Buildx 版本" echo "----------------------------------------" docker buildx version echo "✅ Docker Buildx 可用" echo "" echo "========================================" echo "✅ 所有 Smoke Test 通過!" echo "========================================" REMOTE_EOF echo "========================================" # ======================================== # Step 6: 準備遠端工作目錄 # 說明:確保遠端工作目錄存在 # ======================================== - name: 📁 準備遠端工作目錄 run: | echo "========================================" echo "📁 準備遠端工作目錄" echo "========================================" echo "工作目錄: ${{ env.REMOTE_WORKDIR }}" echo "" tailscale ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} \ "mkdir -p ${{ env.REMOTE_WORKDIR }} && echo '✅ 工作目錄已就緒'" echo "========================================" # ======================================== # Step 7: Clone 或更新專案程式碼 # 說明:在遠端主機上 clone 或 pull 最新程式碼 # ======================================== - name: 📥 Clone 或更新專案程式碼 run: | echo "========================================" echo "📥 Clone 或更新專案程式碼" echo "========================================" tailscale ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'REMOTE_EOF' set -e WORKDIR="${{ env.REMOTE_WORKDIR }}" PROJECT="${{ env.PROJECT_NAME }}" REPO_URL="https://github.com/pi-docket/ConvertX-CN.git" echo "📂 進入工作目錄: ${WORKDIR}" cd "${WORKDIR}" if [ -d "${PROJECT}" ]; then echo "📦 專案已存在,執行 git pull..." cd "${PROJECT}" git fetch origin git reset --hard origin/main git pull origin main echo "✅ 程式碼更新完成" else echo "📦 專案不存在,執行 git clone..." git clone "${REPO_URL}" "${PROJECT}" cd "${PROJECT}" echo "✅ 專案 clone 完成" fi echo "" echo "📋 目前 commit:" git log -1 --oneline echo "" echo "✅ 程式碼準備完成" REMOTE_EOF echo "========================================" # ======================================== # Step 8: Docker Login # 說明:在遠端主機登入 Docker Hub # ======================================== - name: 🔐 Docker Hub 登入 run: | echo "========================================" echo "🔐 Docker Hub 登入" echo "========================================" tailscale ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} \ "echo '${{ secrets.DOCKERHUB_TOKEN }}' | docker login -u '${{ secrets.DOCKERHUB_USERNAME }}' --password-stdin && echo '✅ Docker Hub 登入成功'" echo "========================================" # ======================================== # Step 9: 設定 Docker Buildx(Multi-Arch 必要) # 說明:建立支援多架構的 buildx builder # ======================================== - name: 🔧 設定 Docker Buildx run: | echo "========================================" echo "🔧 設定 Docker Buildx(Multi-Arch 支援)" echo "========================================" tailscale ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'REMOTE_EOF' set -e BUILDER_NAME="multiarch-builder-${{ github.run_id }}" echo "📋 檢查現有 buildx builders..." docker buildx ls echo "" echo "🔧 建立新的 multi-arch builder: ${BUILDER_NAME}" docker buildx create --name "${BUILDER_NAME}" --driver docker-container --use --bootstrap echo "" echo "✅ Buildx builder 已就緒" echo "" echo "📋 目前 builder 狀態:" docker buildx inspect --bootstrap echo "" echo "🏗️ 支援的平台:" docker buildx inspect | grep -i platforms || true REMOTE_EOF echo "========================================" # ======================================== # Step 10: Multi-Arch Docker Build # 說明:使用 buildx 同時建構 amd64 和 arm64 # ======================================== - name: 🔨 Multi-Arch Docker Build run: | echo "========================================" echo "🔨 開始 Multi-Arch Docker Build" echo "========================================" echo "版本: v${{ github.event.inputs.image_tag }}" echo "映像名稱: ${{ env.DOCKER_IMAGE_REPO }}:${{ github.event.inputs.image_tag }}" # 判斷是否為正式版本(純數字+點號,支援任意段數如 0.1.10 或 0.1.10.1) IMAGE_TAG="${{ github.event.inputs.image_tag }}" if [[ "$IMAGE_TAG" =~ ^[0-9]+(\.[0-9]+)+$ ]]; then echo "✅ 正式版本,將同時標記 latest" ADD_LATEST="true" else echo "⚠️ 測試版本,不標記 latest" ADD_LATEST="false" fi echo "目標架構: linux/amd64, linux/arm64" echo "" tailscale ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << REMOTE_EOF set -e WORKDIR="${{ env.REMOTE_WORKDIR }}" PROJECT="${{ env.PROJECT_NAME }}" IMAGE="${{ env.DOCKER_IMAGE_REPO }}:${{ github.event.inputs.image_tag }}" IMAGE_LATEST="${{ env.DOCKER_IMAGE_REPO }}:latest" BUILDER_NAME="multiarch-builder-${{ github.run_id }}" ADD_LATEST="${ADD_LATEST}" echo "📂 進入專案目錄..." cd "\${WORKDIR}/\${PROJECT}" pwd echo "" echo "🔨 執行 Multi-Arch Docker buildx..." echo "映像標籤: \${IMAGE}" echo "目標平台: linux/amd64, linux/arm64" echo "" # 確保使用正確的 builder docker buildx use "\${BUILDER_NAME}" # 根據版本類型決定是否加入 latest tag if [ "\${ADD_LATEST}" = "true" ]; then echo "📌 同時標記 latest" BUILD_TAGS="-t \${IMAGE} -t \${IMAGE_LATEST}" else echo "📌 僅標記版本號(測試版本)" BUILD_TAGS="-t \${IMAGE}" fi # Multi-arch build(只建構,不推送) if ! docker buildx build \ --platform linux/amd64,linux/arm64 \ \${BUILD_TAGS} \ --progress=plain \ . 2>&1; then echo "" echo "========================================" echo "❌ Docker Build 失敗!" echo "========================================" echo "" echo "🔧 常見問題與解決方案:" echo "" echo "1️⃣ apt-get 安裝失敗(arm64 QEMU 問題)" echo " → 建議:拆分 apt-get install 為多段 RUN" echo " → 或:對 arm64 跳過不穩定套件" echo "" echo "2️⃣ 網路問題" echo " → 建議:加入 apt-get update --fix-missing" echo "" echo "3️⃣ 記憶體不足" echo " → 建議:增加 Docker 記憶體限制" echo "" exit 1 fi echo "" echo "✅ Multi-Arch Docker Build 完成!" echo "" echo "🏗️ 已建構的架構:" echo " - linux/amd64" echo " - linux/arm64" REMOTE_EOF echo "========================================" # ======================================== # Step 11: Docker Push 到 Docker Hub # 說明:推送建構好的映像到 Docker Hub # ======================================== - name: 📤 Docker Push 到 Docker Hub run: | echo "========================================" echo "📤 推送映像到 Docker Hub" echo "========================================" echo "版本: v${{ github.event.inputs.image_tag }}" echo "映像: ${{ env.DOCKER_IMAGE_REPO }}:${{ github.event.inputs.image_tag }}" # 判斷是否為正式版本(純數字+點號,支援任意段數如 0.1.10 或 0.1.10.1) IMAGE_TAG="${{ github.event.inputs.image_tag }}" if [[ "$IMAGE_TAG" =~ ^[0-9]+(\.[0-9]+)+$ ]]; then echo "✅ 正式版本,將同時推送 latest" ADD_LATEST="true" else echo "⚠️ 測試版本,不推送 latest" ADD_LATEST="false" fi echo "" # 檢查 Docker Hub 登錄狀態,未登錄則重新登錄 echo "🔐 檢查 Docker Hub 登錄狀態..." tailscale ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << EOF if docker info 2>/dev/null | grep -q "Username:"; then echo "✅ Docker Hub 已登錄" else echo "⚠️ Docker Hub 未登錄,正在重新登錄..." echo '${{ secrets.DOCKERHUB_TOKEN }}' | docker login -u '${{ secrets.DOCKERHUB_USERNAME }}' --password-stdin echo "✅ Docker Hub 重新登錄成功" fi EOF tailscale ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << REMOTE_EOF set -e WORKDIR="${{ env.REMOTE_WORKDIR }}" PROJECT="${{ env.PROJECT_NAME }}" IMAGE="${{ env.DOCKER_IMAGE_REPO }}:${{ github.event.inputs.image_tag }}" IMAGE_LATEST="${{ env.DOCKER_IMAGE_REPO }}:latest" BUILDER_NAME="multiarch-builder-${{ github.run_id }}" ADD_LATEST="${ADD_LATEST}" echo "📂 進入專案目錄..." cd "\${WORKDIR}/\${PROJECT}" echo "" echo "📤 執行 Docker Push..." echo "映像標籤: \${IMAGE}" # 確保使用正確的 builder docker buildx use "\${BUILDER_NAME}" # 根據版本類型決定是否加入 latest tag if [ "\${ADD_LATEST}" = "true" ]; then echo "同時推送: \${IMAGE_LATEST}" BUILD_TAGS="-t \${IMAGE} -t \${IMAGE_LATEST}" else echo "(測試版本,不推送 latest)" BUILD_TAGS="-t \${IMAGE}" fi echo "" # 使用 buildx build --push 來推送 multi-arch 映像 if ! docker buildx build \ --platform linux/amd64,linux/arm64 \ \${BUILD_TAGS} \ --push \ --progress=plain \ . 2>&1; then echo "" echo "========================================" echo "❌ Docker Push 失敗!" echo "========================================" echo "" echo "🔧 可能的原因:" echo " 1. Docker Hub 登入已過期" echo " 2. 網路連線問題" echo " 3. Docker Hub 配額限制" echo "" exit 1 fi echo "" echo "✅ Docker Push 完成!" echo "" echo "📦 映像已推送至 Docker Hub:" echo " \${IMAGE}" if [ "\${ADD_LATEST}" = "true" ]; then echo " \${IMAGE_LATEST}" fi echo "" echo "🏗️ 支援的架構:" echo " - linux/amd64" echo " - linux/arm64" REMOTE_EOF echo "========================================" # ======================================== # Step 13: 驗證 Docker Hub 映像 # 說明:確認映像已成功推送並檢查架構 # ======================================== - name: ✅ 驗證 Docker Hub 映像 run: | echo "========================================" echo "✅ 驗證 Docker Hub 映像" echo "========================================" echo "版本: v${{ github.event.inputs.image_tag }}" echo "映像: ${{ env.DOCKER_IMAGE_REPO }}:${{ github.event.inputs.image_tag }}" echo "" tailscale ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'REMOTE_EOF' set -e IMAGE="${{ env.DOCKER_IMAGE_REPO }}:${{ github.event.inputs.image_tag }}" echo "📋 檢查映像 manifest(顯示支援的架構)..." echo "" docker buildx imagetools inspect "${IMAGE}" echo "" echo "✅ 映像驗證完成" REMOTE_EOF echo "========================================" # ======================================== # Step 14: 清理本次 Build 資源 # 說明:只清理本次 build 產生的 builder 和 cache # ======================================== - name: 🧹 清理本次 Build 資源 if: always() run: | echo "========================================" echo "🧹 清理本次 Build 資源" echo "========================================" echo "Builder 名稱: multiarch-builder-${{ github.run_id }}" echo "" tailscale ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'REMOTE_EOF' set -e BUILDER_NAME="multiarch-builder-${{ github.run_id }}" echo "🗑️ 移除本次使用的 buildx builder..." if docker buildx rm "${BUILDER_NAME}" 2>/dev/null; then echo "✅ Builder ${BUILDER_NAME} 已移除" else echo "⚠️ Builder 可能已被移除或不存在" fi echo "" echo "🗑️ 清理本次 build 產生的 cache..." # 只清理 buildx 的 dangling cache,不影響其他 image docker buildx prune -f --filter "until=1h" 2>/dev/null || true echo "✅ Build cache 已清理" echo "" echo "📋 目前剩餘的 builders:" docker buildx ls echo "" echo "📦 目前剩餘的 convertx 相關映像:" docker images | grep convertx || echo "(無本地 convertx 相關映像,因使用 --push 直接推送)" REMOTE_EOF echo "" echo "✅ 清理完成(僅清理本次 build 的資源)" echo "========================================" # ======================================== # Step 15: Docker Logout # 說明:登出 Docker Hub # ======================================== - name: 🔒 Docker Hub 登出 if: always() run: | echo "========================================" echo "🔒 Docker Hub 登出" echo "========================================" tailscale ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} \ "docker logout && echo '✅ 已登出 Docker Hub'" || true echo "========================================" # ======================================== # Step 16: 顯示完成摘要 # 說明:顯示本次 build 的摘要資訊 # ======================================== - name: 📋 Build 完成摘要 run: | echo "========================================" echo "✅ Multi-Arch Docker Build & Push 完成!" echo "========================================" echo "" echo "🏷️ 版本: v${{ github.event.inputs.image_tag }}" echo "" echo "📦 映像資訊:" echo " - 版本標籤: ${{ env.DOCKER_IMAGE_REPO }}:${{ github.event.inputs.image_tag }}" # 判斷是否為正式版本(純數字+點號) IMAGE_TAG="${{ github.event.inputs.image_tag }}" if [[ "$IMAGE_TAG" =~ ^[0-9]+(\.[0-9]+)+$ ]]; then echo " - Latest 標籤: ${{ env.DOCKER_IMAGE_REPO }}:latest ✅" else echo " - Latest 標籤: (測試版本,未更新)" fi echo " - Docker Hub: https://hub.docker.com/r/${{ env.DOCKER_IMAGE_REPO }}/tags" echo "" echo "🏗️ 支援的架構:" echo " - linux/amd64" echo " - linux/arm64" echo "" echo "🔧 Build 設定:" echo " - 遠端主機: ${{ secrets.SSH_HOST }}" echo " - 工作目錄: ${{ env.REMOTE_WORKDIR }}" echo " - 專案名稱: ${{ env.PROJECT_NAME }}" echo "" echo "📝 使用方式:" echo " docker pull ${{ env.DOCKER_IMAGE_REPO }}:${{ github.event.inputs.image_tag }}" if [[ "$IMAGE_TAG" =~ ^[0-9]+(\.[0-9]+)+$ ]]; then echo " docker pull ${{ env.DOCKER_IMAGE_REPO }}:latest" fi echo "" echo "========================================" echo "🎉 所有步驟已成功完成!" echo "========================================" # ======================================== # Step 17: 生成 Changelog # 說明:從 git log 生成變更記錄(與 release.yml 邏輯一致) # ======================================== - name: 📝 Generate Changelog if: ${{ !contains(github.event.inputs.image_tag, 'test') && !contains(github.event.inputs.image_tag, 'dev') && !contains(github.event.inputs.image_tag, 'alpha') && !contains(github.event.inputs.image_tag, 'beta') }} id: changelog run: | echo "========================================" echo "📝 生成 Changelog" echo "========================================" # 取得完整 git history git fetch --unshallow 2>/dev/null || git fetch --all # 取得前一個 tag PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") if [ -z "$PREVIOUS_TAG" ]; then echo "📋 無前一個 tag,取得所有 commits..." CHANGELOG=$(git log --pretty=format:"- %s (%h)" HEAD) else echo "📋 取得 ${PREVIOUS_TAG} 到 HEAD 的 commits..." CHANGELOG=$(git log --pretty=format:"- %s (%h)" ${PREVIOUS_TAG}..HEAD) fi # 建立 changelog 檔案(與 release.yml 風格一致) echo "$CHANGELOG" > changelog.txt # 加入 Full Changelog 連結 echo "" >> changelog.txt echo "Full Changelog: https://github.com/${{ github.repository }}/compare/${PREVIOUS_TAG}...v${{ github.event.inputs.image_tag }}" >> changelog.txt echo "" echo "✅ Changelog 已生成:" cat changelog.txt echo "========================================" # ======================================== # Step 18: 創建 GitHub Release # 說明:為正式版本自動創建 GitHub Release # 注意:不使用 generate_release_notes,改用 body_path 提供完整 changelog # 注意:如果 Release 已存在會失敗,使用 continue-on-error 跳過 # ======================================== - name: 📦 Create GitHub Release if: ${{ !contains(github.event.inputs.image_tag, 'test') && !contains(github.event.inputs.image_tag, 'dev') && !contains(github.event.inputs.image_tag, 'alpha') && !contains(github.event.inputs.image_tag, 'beta') }} continue-on-error: true uses: softprops/action-gh-release@v2 with: tag_name: v${{ github.event.inputs.image_tag }} name: v${{ github.event.inputs.image_tag }} body_path: changelog.txt draft: false prerelease: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # ======================================== # Step 19: 更新遠端 ConvertX-CN 服務 # 說明:拉取最新映像並重新啟動服務(所有步驟完成後執行) # ======================================== - name: 🔄 更新遠端 ConvertX-CN 服務 run: | echo "========================================" echo "🔄 更新遠端 ConvertX-CN 服務" echo "========================================" echo "版本: v${{ github.event.inputs.image_tag }}" echo "" tailscale ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << REMOTE_EOF set -e IMAGE_TAG="v${{ github.event.inputs.image_tag }}" # 1. 進入專案資料夾 cd /home/bioailab/miniconda3/lid/app/convertx-cn # 2. 停止服務 echo "📋 停止服務..." docker compose down # 3. 拉取最新映像檔 echo "" echo "📋 拉取最新映像檔(\${IMAGE_TAG})..." docker compose pull # 4. 重新啟動 echo "" echo "📋 重新啟動服務..." docker compose up -d # 5. 顯示服務狀態與 log echo "" echo "📋 服務狀態:" docker compose ps echo "" echo "📋 服務 log(最近 100 行):" docker logs convertx-cn --tail=100 echo "" echo "✅ ConvertX-CN 服務已更新至 \${IMAGE_TAG}" REMOTE_EOF echo "========================================" # ======================================== # Step 20: Docker 容器 E2E 測試 # 說明:在已更新的遠端服務上執行完整 E2E 測試 # ======================================== - name: 🧪 Docker 容器 E2E 測試 run: | echo "========================================" echo "🧪 Docker 容器 E2E 測試(已部署服務)" echo "========================================" echo "版本: v${{ github.event.inputs.image_tag }}" echo "" tailscale ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'REMOTE_EOF' set -e CONTAINER_NAME="convertx-cn" echo "⏳ 等待服務就緒..." for i in {1..60}; do if docker exec "${CONTAINER_NAME}" curl -sf http://localhost:3000/healthcheck > /dev/null 2>&1; then echo "✅ 服務在 ${i} 秒內就緒" break fi if [ $i -eq 60 ]; then echo "❌ 服務啟動超時" docker logs "${CONTAINER_NAME}" --tail=50 exit 1 fi sleep 1 done echo "" echo "========================================" echo "📋 執行 E2E 轉換測試..." echo "========================================" FAILED=0 PASSED=0 # 測試 1: Inkscape SVG → PNG docker exec "${CONTAINER_NAME}" sh -c ' echo "" > /tmp/test.svg inkscape --export-type=png --export-filename=/tmp/test.png /tmp/test.svg 2>/dev/null [ -f /tmp/test.png ] && echo "✅ Inkscape SVG → PNG" || exit 1 ' && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) # 測試 2: Inkscape SVG → PDF docker exec "${CONTAINER_NAME}" sh -c ' inkscape --export-type=pdf --export-filename=/tmp/test.pdf /tmp/test.svg 2>/dev/null [ -f /tmp/test.pdf ] && echo "✅ Inkscape SVG → PDF" || exit 1 ' && PASSED=$((PASSED + 1)) || echo "⚠️ Inkscape SVG → PDF 跳過" # 測試 3: Pandoc Markdown → HTML docker exec "${CONTAINER_NAME}" sh -c ' echo "# Test\n\nHello **world**" > /tmp/test.md pandoc /tmp/test.md -o /tmp/test.html [ -f /tmp/test.html ] && echo "✅ Pandoc Markdown → HTML" || exit 1 ' && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) # 測試 4: Pandoc Markdown → DOCX docker exec "${CONTAINER_NAME}" sh -c ' pandoc /tmp/test.md -o /tmp/test_pandoc.docx [ -f /tmp/test_pandoc.docx ] && echo "✅ Pandoc Markdown → DOCX" || exit 1 ' && PASSED=$((PASSED + 1)) || echo "⚠️ Pandoc DOCX 跳過" # 測試 5: Pandoc Markdown → LaTeX docker exec "${CONTAINER_NAME}" sh -c ' pandoc /tmp/test.md -o /tmp/test.tex [ -f /tmp/test.tex ] && echo "✅ Pandoc Markdown → LaTeX" || exit 1 ' && PASSED=$((PASSED + 1)) || echo "⚠️ Pandoc LaTeX 跳過" # 測試 6: LibreOffice TXT → PDF docker exec "${CONTAINER_NAME}" sh -c ' echo "Test document content" > /tmp/test.txt libreoffice --headless --convert-to pdf --outdir /tmp /tmp/test.txt 2>/dev/null [ -f /tmp/test.pdf ] && echo "✅ LibreOffice TXT → PDF" || exit 1 ' && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) # 測試 7: LibreOffice TXT → ODT → DOCX docker exec "${CONTAINER_NAME}" sh -c ' libreoffice --headless --convert-to odt --outdir /tmp /tmp/test.txt 2>/dev/null if [ -f /tmp/test.odt ]; then libreoffice --headless --convert-to docx --outdir /tmp /tmp/test.odt 2>/dev/null [ -f /tmp/test.docx ] && echo "✅ LibreOffice ODT → DOCX" || exit 1 fi ' && PASSED=$((PASSED + 1)) || echo "⚠️ LibreOffice ODT 跳過" # 測試 8: FFmpeg 音頻生成 docker exec "${CONTAINER_NAME}" sh -c ' ffmpeg -f lavfi -i "sine=frequency=440:duration=1" -y /tmp/test.wav 2>/dev/null [ -f /tmp/test.wav ] && echo "✅ FFmpeg 音頻生成" || exit 1 ' && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) # 測試 9: FFmpeg WAV → MP3 docker exec "${CONTAINER_NAME}" sh -c ' ffmpeg -i /tmp/test.wav -y /tmp/test.mp3 2>/dev/null [ -f /tmp/test.mp3 ] && echo "✅ FFmpeg WAV → MP3" || exit 1 ' && PASSED=$((PASSED + 1)) || echo "⚠️ FFmpeg MP3 跳過" # 測試 10: FFmpeg 影片生成 docker exec "${CONTAINER_NAME}" sh -c ' ffmpeg -f lavfi -i color=c=red:s=320x240:d=1 -y /tmp/test_video.mp4 2>/dev/null [ -f /tmp/test_video.mp4 ] && echo "✅ FFmpeg 影片生成" || exit 1 ' && PASSED=$((PASSED + 1)) || echo "⚠️ FFmpeg 影片 跳過" # 測試 11: FFmpeg MP4 → GIF docker exec "${CONTAINER_NAME}" sh -c ' if [ -f /tmp/test_video.mp4 ]; then ffmpeg -i /tmp/test_video.mp4 -vf "fps=10,scale=160:-1" -y /tmp/test_video.gif 2>/dev/null [ -f /tmp/test_video.gif ] && echo "✅ FFmpeg MP4 → GIF" || exit 1 fi ' && PASSED=$((PASSED + 1)) || echo "⚠️ FFmpeg GIF 跳過" # 測試 12: ImageMagick 圖片生成 docker exec "${CONTAINER_NAME}" sh -c ' convert -size 100x100 xc:blue /tmp/blue.png [ -f /tmp/blue.png ] && echo "✅ ImageMagick 圖片生成" || exit 1 ' && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) # 測試 13: ImageMagick PNG → JPEG docker exec "${CONTAINER_NAME}" sh -c ' convert /tmp/blue.png /tmp/blue.jpg [ -f /tmp/blue.jpg ] && echo "✅ ImageMagick PNG → JPEG" || exit 1 ' && PASSED=$((PASSED + 1)) || echo "⚠️ ImageMagick JPEG 跳過" # 測試 14: ImageMagick PNG → WEBP docker exec "${CONTAINER_NAME}" sh -c ' convert /tmp/blue.png /tmp/blue.webp [ -f /tmp/blue.webp ] && echo "✅ ImageMagick PNG → WEBP" || exit 1 ' && PASSED=$((PASSED + 1)) || echo "⚠️ ImageMagick WEBP 跳過" # 測試 15: Calibre HTML → EPUB docker exec "${CONTAINER_NAME}" sh -c ' if command -v ebook-convert &> /dev/null; then echo "

Test

" > /tmp/book.html ebook-convert /tmp/book.html /tmp/book.epub 2>/dev/null [ -f /tmp/book.epub ] && echo "✅ Calibre HTML → EPUB" || exit 1 else exit 1 fi ' && PASSED=$((PASSED + 1)) || echo "⚠️ Calibre 跳過" # 測試 16: Potrace PBM → SVG docker exec "${CONTAINER_NAME}" sh -c ' if command -v potrace &> /dev/null; then 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/potrace.svg /tmp/test.pbm [ -f /tmp/potrace.svg ] && echo "✅ Potrace PBM → SVG" || exit 1 else exit 1 fi ' && PASSED=$((PASSED + 1)) || echo "⚠️ Potrace 跳過" # 測試 17: Tesseract OCR docker exec "${CONTAINER_NAME}" sh -c ' if command -v tesseract &> /dev/null; then convert -size 200x50 xc:white -font DejaVu-Sans -pointsize 20 -fill black -annotate +10+35 "Test" /tmp/ocr.png 2>/dev/null tesseract /tmp/ocr.png /tmp/ocr 2>/dev/null [ -f /tmp/ocr.txt ] && echo "✅ Tesseract OCR" || exit 1 else exit 1 fi ' && PASSED=$((PASSED + 1)) || echo "⚠️ Tesseract 跳過" # 測試 18: GraphicsMagick docker exec "${CONTAINER_NAME}" sh -c ' if command -v gm &> /dev/null; then gm convert -size 100x100 xc:green /tmp/gm.png gm convert /tmp/gm.png /tmp/gm.gif [ -f /tmp/gm.gif ] && echo "✅ GraphicsMagick PNG → GIF" || exit 1 else exit 1 fi ' && PASSED=$((PASSED + 1)) || echo "⚠️ GraphicsMagick 跳過" # 測試 19: Resvg docker exec "${CONTAINER_NAME}" sh -c ' if command -v resvg &> /dev/null; then resvg /tmp/test.svg /tmp/resvg.png [ -f /tmp/resvg.png ] && echo "✅ Resvg SVG → PNG" || exit 1 else exit 1 fi ' && PASSED=$((PASSED + 1)) || echo "⚠️ Resvg 跳過" # 測試 20: VIPS docker exec "${CONTAINER_NAME}" sh -c ' if command -v vips &> /dev/null; then vips copy /tmp/blue.png /tmp/vips.webp [ -f /tmp/vips.webp ] && echo "✅ VIPS PNG → WEBP" || exit 1 else exit 1 fi ' && PASSED=$((PASSED + 1)) || echo "⚠️ VIPS 跳過" # 測試 21: API Healthcheck if docker exec "${CONTAINER_NAME}" curl -sf http://localhost:3000/healthcheck | grep -q "OK"; then echo "✅ API Healthcheck" PASSED=$((PASSED + 1)) else echo "❌ API Healthcheck 失敗" FAILED=$((FAILED + 1)) fi # 結果摘要 echo "" echo "========================================" echo "📊 E2E 測試結果" echo "========================================" echo "✅ 通過: ${PASSED} 項" echo "⚠️ 跳過/失敗: 部分可選測試" echo "" if [ $FAILED -gt 3 ]; then echo "❌ 關鍵測試失敗過多!" exit 1 else echo "✅ E2E 測試通過!" fi echo "========================================" REMOTE_EOF echo "========================================"