- 將 Python 安裝分成多個 RUN 步驟 - 修正 apt-get purge 在 lists 清除後失敗的問題 - 添加動態版本號修改步驟到 docker-build-lite.yml - Lite 版 UI 將顯示 X.Y.Z-lite 版本號
1111 lines
44 KiB
YAML
1111 lines
44 KiB
YAML
# ============================================================
|
||
# 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 與 build target
|
||
workflow_dispatch:
|
||
inputs:
|
||
image_tag:
|
||
description: "📦 版本號(例如:0.1.15、0.1.15-lite、0.1.15.test)"
|
||
required: true
|
||
default: "0.1.15.test"
|
||
type: string
|
||
build_target:
|
||
description: "🎯 Build 目標版本(auto = 自動從版本號偵測)"
|
||
required: true
|
||
default: "auto"
|
||
type: choice
|
||
options:
|
||
- auto
|
||
- standard
|
||
- full
|
||
- lite
|
||
|
||
# ============================================================
|
||
# 環境變數設定
|
||
# ============================================================
|
||
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 0: 解析版本快捷選項
|
||
# 說明:根據 version_preset 自動設定 IMAGE_TAG 和 BUILD_TARGET
|
||
# ========================================
|
||
- name: 🔧 解析版本設定
|
||
id: parse_version
|
||
run: |
|
||
echo "========================================"
|
||
echo "🔧 解析版本設定"
|
||
echo "========================================"
|
||
|
||
INPUT_TAG="${{ github.event.inputs.image_tag }}"
|
||
BUILD_TARGET_INPUT="${{ github.event.inputs.build_target }}"
|
||
|
||
echo "輸入參數:"
|
||
echo " image_tag: ${INPUT_TAG}"
|
||
echo " build_target: ${BUILD_TARGET_INPUT}"
|
||
echo ""
|
||
|
||
# 驗證版本號不為空
|
||
if [ -z "${INPUT_TAG}" ]; then
|
||
echo "❌ 錯誤:必須填寫版本號"
|
||
exit 1
|
||
fi
|
||
|
||
# ========================================
|
||
# 判斷 build target(auto 時根據 tag 自動判斷)
|
||
# ========================================
|
||
if [ "${BUILD_TARGET_INPUT}" = "auto" ]; then
|
||
if [[ "${INPUT_TAG}" == *"-lite"* ]]; then
|
||
FINAL_BUILD_TARGET="lite"
|
||
echo "🔍 自動偵測:版本號含 '-lite',選擇 Lite 版"
|
||
elif [[ "${INPUT_TAG}" == *"-full"* ]]; then
|
||
FINAL_BUILD_TARGET="full"
|
||
echo "🔍 自動偵測:版本號含 '-full',選擇 Full 版"
|
||
else
|
||
FINAL_BUILD_TARGET="standard"
|
||
echo "🔍 自動偵測:標準版本號,選擇一般版"
|
||
fi
|
||
else
|
||
FINAL_BUILD_TARGET="${BUILD_TARGET_INPUT}"
|
||
fi
|
||
|
||
# ========================================
|
||
# 根據 build target 決定最終 tag
|
||
# ========================================
|
||
# 移除可能的後綴以獲取基礎版本號
|
||
BASE_TAG="${INPUT_TAG%-lite}"
|
||
BASE_TAG="${BASE_TAG%-full}"
|
||
|
||
case "${FINAL_BUILD_TARGET}" in
|
||
"lite")
|
||
FINAL_TAG="${BASE_TAG}-lite"
|
||
DOCKERFILE="Dockerfile.lite"
|
||
LATEST_TAG="latest-lite"
|
||
EDITION="Lite 版(輕量版)"
|
||
;;
|
||
"full")
|
||
FINAL_TAG="${BASE_TAG}-full"
|
||
DOCKERFILE="Dockerfile.full"
|
||
LATEST_TAG="latest-full"
|
||
EDITION="Full 版(擴充版)"
|
||
;;
|
||
*)
|
||
FINAL_TAG="${BASE_TAG}"
|
||
DOCKERFILE="Dockerfile"
|
||
LATEST_TAG="latest"
|
||
EDITION="一般版(標準版)"
|
||
;;
|
||
esac
|
||
|
||
# ========================================
|
||
# 判斷是否為正式版本(決定是否更新 latest)
|
||
# 正式版本格式:純數字+點號(如 0.1.15、0.1.15.1)
|
||
# 測試版本格式:含有 test/dev/alpha/beta 或其他非數字字元
|
||
# ========================================
|
||
if [[ "${BASE_TAG}" =~ ^[0-9]+(\.[0-9]+)+$ ]]; then
|
||
IS_OFFICIAL="true"
|
||
echo "✅ 正式版本:將同時更新 ${LATEST_TAG}"
|
||
else
|
||
IS_OFFICIAL="false"
|
||
echo "⚠️ 測試版本:不更新 ${LATEST_TAG}"
|
||
fi
|
||
|
||
echo ""
|
||
echo "========================================"
|
||
echo "📋 最終設定:"
|
||
echo "========================================"
|
||
echo " 版本號: ${FINAL_TAG}"
|
||
echo " Build 目標: ${FINAL_BUILD_TARGET}"
|
||
echo " Dockerfile: ${DOCKERFILE}"
|
||
echo " 版本類型: ${EDITION}"
|
||
echo " Latest Tag: ${LATEST_TAG}"
|
||
echo " 正式版本: ${IS_OFFICIAL}"
|
||
echo "========================================"
|
||
|
||
# 輸出到 GITHUB_OUTPUT
|
||
echo "image_tag=${FINAL_TAG}" >> $GITHUB_OUTPUT
|
||
echo "base_tag=${BASE_TAG}" >> $GITHUB_OUTPUT
|
||
echo "build_target=${FINAL_BUILD_TARGET}" >> $GITHUB_OUTPUT
|
||
echo "dockerfile=${DOCKERFILE}" >> $GITHUB_OUTPUT
|
||
echo "latest_tag=${LATEST_TAG}" >> $GITHUB_OUTPUT
|
||
echo "edition=${EDITION}" >> $GITHUB_OUTPUT
|
||
echo "is_official=${IS_OFFICIAL}" >> $GITHUB_OUTPUT
|
||
|
||
# ========================================
|
||
# 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
|
||
# 支援 full/lite 版本選擇
|
||
# ========================================
|
||
- name: 🔨 Multi-Arch Docker Build
|
||
run: |
|
||
echo "========================================"
|
||
echo "🔨 開始 Multi-Arch Docker Build"
|
||
echo "========================================"
|
||
|
||
# 使用 parse_version 步驟的輸出
|
||
FINAL_TAG="${{ steps.parse_version.outputs.image_tag }}"
|
||
DOCKERFILE="${{ steps.parse_version.outputs.dockerfile }}"
|
||
LATEST_TAG="${{ steps.parse_version.outputs.latest_tag }}"
|
||
IS_OFFICIAL="${{ steps.parse_version.outputs.is_official }}"
|
||
EDITION="${{ steps.parse_version.outputs.edition }}"
|
||
|
||
echo "🔹 Build 目標: ${EDITION}"
|
||
echo "🔹 使用 Dockerfile: ${DOCKERFILE}"
|
||
echo "🔹 版本: v${FINAL_TAG}"
|
||
echo "🔹 映像名稱: ${{ env.DOCKER_IMAGE_REPO }}:${FINAL_TAG}"
|
||
|
||
if [ "${IS_OFFICIAL}" = "true" ]; then
|
||
echo "✅ 正式版本,將同時標記 ${LATEST_TAG}"
|
||
ADD_LATEST="true"
|
||
else
|
||
echo "⚠️ 測試版本,不標記 ${LATEST_TAG}"
|
||
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 }}:${FINAL_TAG}"
|
||
IMAGE_LATEST="${{ env.DOCKER_IMAGE_REPO }}:${LATEST_TAG}"
|
||
BUILDER_NAME="multiarch-builder-${{ github.run_id }}"
|
||
ADD_LATEST="${ADD_LATEST}"
|
||
DOCKERFILE="${DOCKERFILE}"
|
||
|
||
echo "📂 進入專案目錄..."
|
||
cd "\${WORKDIR}/\${PROJECT}"
|
||
pwd
|
||
|
||
echo ""
|
||
echo "🔨 執行 Multi-Arch Docker buildx..."
|
||
echo "映像標籤: \${IMAGE}"
|
||
echo "Dockerfile: \${DOCKERFILE}"
|
||
echo "目標平台: linux/amd64, linux/arm64"
|
||
echo ""
|
||
|
||
# 確保使用正確的 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 "📌 僅標記版本號(測試版本)"
|
||
BUILD_TAGS="-t \${IMAGE}"
|
||
fi
|
||
|
||
# Multi-arch build(只建構,不推送)
|
||
if ! docker buildx build \\
|
||
--platform linux/amd64,linux/arm64 \\
|
||
-f \${DOCKERFILE} \\
|
||
\${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
|
||
# 支援 standard/full/lite 版本選擇
|
||
# ========================================
|
||
- name: 📤 Docker Push 到 Docker Hub
|
||
run: |
|
||
echo "========================================"
|
||
echo "📤 推送映像到 Docker Hub"
|
||
echo "========================================"
|
||
|
||
# 使用 parse_version 步驟的輸出
|
||
FINAL_TAG="${{ steps.parse_version.outputs.image_tag }}"
|
||
DOCKERFILE="${{ steps.parse_version.outputs.dockerfile }}"
|
||
LATEST_TAG="${{ steps.parse_version.outputs.latest_tag }}"
|
||
IS_OFFICIAL="${{ steps.parse_version.outputs.is_official }}"
|
||
EDITION="${{ steps.parse_version.outputs.edition }}"
|
||
|
||
echo "🔹 Push 目標: ${EDITION}"
|
||
echo "🔹 版本: v${FINAL_TAG}"
|
||
echo "🔹 映像: ${{ env.DOCKER_IMAGE_REPO }}:${FINAL_TAG}"
|
||
|
||
if [ "${IS_OFFICIAL}" = "true" ]; then
|
||
echo "✅ 正式版本,將同時推送 ${LATEST_TAG}"
|
||
ADD_LATEST="true"
|
||
else
|
||
echo "⚠️ 測試版本,不推送 ${LATEST_TAG}"
|
||
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 }}:${FINAL_TAG}"
|
||
IMAGE_LATEST="${{ env.DOCKER_IMAGE_REPO }}:${LATEST_TAG}"
|
||
BUILDER_NAME="multiarch-builder-${{ github.run_id }}"
|
||
ADD_LATEST="${ADD_LATEST}"
|
||
DOCKERFILE="${DOCKERFILE}"
|
||
|
||
echo "📂 進入專案目錄..."
|
||
cd "\${WORKDIR}/\${PROJECT}"
|
||
|
||
echo ""
|
||
echo "📤 執行 Docker Push..."
|
||
echo "映像標籤: \${IMAGE}"
|
||
echo "Dockerfile: \${DOCKERFILE}"
|
||
|
||
# 確保使用正確的 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_TAG})"
|
||
BUILD_TAGS="-t \${IMAGE}"
|
||
fi
|
||
echo ""
|
||
|
||
# 使用 buildx build --push 來推送 multi-arch 映像
|
||
if ! docker buildx build \
|
||
--platform linux/amd64,linux/arm64 \
|
||
-f \${DOCKERFILE} \
|
||
\${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 映像
|
||
# 說明:確認映像已成功推送並檢查架構
|
||
# 支援 standard/full/lite 版本
|
||
# ========================================
|
||
- name: ✅ 驗證 Docker Hub 映像
|
||
run: |
|
||
echo "========================================"
|
||
echo "✅ 驗證 Docker Hub 映像"
|
||
echo "========================================"
|
||
|
||
FINAL_TAG="${{ steps.parse_version.outputs.image_tag }}"
|
||
EDITION="${{ steps.parse_version.outputs.edition }}"
|
||
|
||
echo "🔹 版本類型: ${EDITION}"
|
||
echo "🔹 版本: v${FINAL_TAG}"
|
||
echo "🔹 映像: ${{ env.DOCKER_IMAGE_REPO }}:${FINAL_TAG}"
|
||
echo ""
|
||
|
||
tailscale ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << REMOTE_EOF
|
||
set -e
|
||
|
||
IMAGE="${{ env.DOCKER_IMAGE_REPO }}:${FINAL_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 的摘要資訊
|
||
# 支援 standard/full/lite 版本
|
||
# ========================================
|
||
- name: 📋 Build 完成摘要
|
||
run: |
|
||
echo "========================================"
|
||
echo "✅ Multi-Arch Docker Build & Push 完成!"
|
||
echo "========================================"
|
||
echo ""
|
||
|
||
# 使用 parse_version 步驟的輸出
|
||
FINAL_TAG="${{ steps.parse_version.outputs.image_tag }}"
|
||
BASE_TAG="${{ steps.parse_version.outputs.base_tag }}"
|
||
DOCKERFILE="${{ steps.parse_version.outputs.dockerfile }}"
|
||
LATEST_TAG="${{ steps.parse_version.outputs.latest_tag }}"
|
||
IS_OFFICIAL="${{ steps.parse_version.outputs.is_official }}"
|
||
EDITION="${{ steps.parse_version.outputs.edition }}"
|
||
|
||
echo "🏷️ 版本: v${FINAL_TAG}"
|
||
echo "📦 版本類型: ${EDITION}"
|
||
echo ""
|
||
echo "📦 映像資訊:"
|
||
echo " - 版本標籤: ${{ env.DOCKER_IMAGE_REPO }}:${FINAL_TAG}"
|
||
|
||
# 判斷是否為正式版本
|
||
if [ "${IS_OFFICIAL}" = "true" ]; then
|
||
echo " - Latest 標籤: ${{ env.DOCKER_IMAGE_REPO }}:${LATEST_TAG} ✅"
|
||
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 " - Dockerfile: ${DOCKERFILE}"
|
||
echo ""
|
||
echo "📝 使用方式:"
|
||
echo " docker pull ${{ env.DOCKER_IMAGE_REPO }}:${FINAL_TAG}"
|
||
if [ "${IS_OFFICIAL}" = "true" ]; then
|
||
echo " docker pull ${{ env.DOCKER_IMAGE_REPO }}:${LATEST_TAG}"
|
||
fi
|
||
echo ""
|
||
echo "========================================"
|
||
echo "🎉 所有步驟已成功完成!"
|
||
echo "========================================"
|
||
|
||
# ========================================
|
||
# Step 17: 生成 Changelog
|
||
# 說明:從 git log 生成變更記錄(與 release.yml 邏輯一致)
|
||
# ========================================
|
||
- name: 📝 Generate Changelog
|
||
if: ${{ !contains(steps.parse_version.outputs.image_tag, 'test') && !contains(steps.parse_version.outputs.image_tag, 'dev') && !contains(steps.parse_version.outputs.image_tag, 'alpha') && !contains(steps.parse_version.outputs.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(steps.parse_version.outputs.image_tag, 'test') && !contains(steps.parse_version.outputs.image_tag, 'dev') && !contains(steps.parse_version.outputs.image_tag, 'alpha') && !contains(steps.parse_version.outputs.image_tag, 'beta') }}
|
||
continue-on-error: true
|
||
uses: softprops/action-gh-release@v2
|
||
with:
|
||
tag_name: v${{ steps.parse_version.outputs.image_tag }}
|
||
name: v${{ steps.parse_version.outputs.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${{ steps.parse_version.outputs.image_tag }}"
|
||
echo ""
|
||
|
||
tailscale ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << REMOTE_EOF
|
||
set -e
|
||
|
||
IMAGE_TAG="v${{ steps.parse_version.outputs.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 "<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
|
||
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 "<!DOCTYPE html><html><body><h1>Test</h1></body></html>" > /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 "========================================"
|