feat: 更新 Remote Build workflow 支援 Multi-Arch (amd64/arm64)
This commit is contained in:
parent
de11dd5a0d
commit
41a7111db2
1 changed files with 362 additions and 58 deletions
420
.github/workflows/docker-build-remote.yml
vendored
420
.github/workflows/docker-build-remote.yml
vendored
|
|
@ -1,92 +1,396 @@
|
||||||
name: Docker Build & Push (Remote Host)
|
# ============================================================
|
||||||
|
# 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:
|
on:
|
||||||
|
# 手動觸發,支援輸入 image tag
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
inputs:
|
||||||
branches:
|
image_tag:
|
||||||
- main
|
description: "Docker Image Tag(例如:0.1.10.test)"
|
||||||
|
required: true
|
||||||
|
default: "0.1.10.test"
|
||||||
|
type: string
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 環境變數設定
|
||||||
|
# ============================================================
|
||||||
env:
|
env:
|
||||||
|
# 遠端主機工作目錄
|
||||||
REMOTE_WORKDIR: /home/bioailab/miniconda3/lid/app/docker_build
|
REMOTE_WORKDIR: /home/bioailab/miniconda3/lid/app/docker_build
|
||||||
|
# 專案名稱
|
||||||
PROJECT_NAME: ConvertX-CN
|
PROJECT_NAME: ConvertX-CN
|
||||||
|
# Docker Hub 映像庫
|
||||||
DOCKER_IMAGE_REPO: convertx/convertx-cn
|
DOCKER_IMAGE_REPO: convertx/convertx-cn
|
||||||
IMAGE_TAG: 0.1.10.test
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
build-and-push:
|
||||||
|
name: 🚀 遠端 Docker Build & Push
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[remote-build]')
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Setup SSH Key
|
# ========================================
|
||||||
|
# Step 1: Checkout repository
|
||||||
|
# 說明:檢出程式碼(雖然 build 在遠端,但需要 workflow 完整性)
|
||||||
|
# ========================================
|
||||||
|
- name: 📥 Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Step 2: 啟動 Tailscale
|
||||||
|
# 說明:使用 Tailscale Auth Key 加入 tailnet
|
||||||
|
# ========================================
|
||||||
|
- name: 🔗 啟動 Tailscale 連線
|
||||||
|
uses: tailscale/github-action@v2
|
||||||
|
with:
|
||||||
|
authkey: ${{ secrets.TAILSCALE_AUTHKEY }}
|
||||||
|
# 使用 ephemeral key,runner 結束後自動離開 tailnet
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Step 3: 驗證 Tailscale 連線
|
||||||
|
# 說明:確認 Tailscale 已成功連線並顯示狀態
|
||||||
|
# ========================================
|
||||||
|
- name: ✅ 驗證 Tailscale 連線狀態
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.ssh
|
echo "========================================"
|
||||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
echo "🔍 檢查 Tailscale 連線狀態"
|
||||||
chmod 600 ~/.ssh/id_rsa
|
echo "========================================"
|
||||||
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts 2>/dev/null || true
|
|
||||||
|
|
||||||
- name: Build and Push Docker Image on Remote Host
|
# 顯示 Tailscale 狀態
|
||||||
run: |
|
echo ""
|
||||||
# Export variables for use in SSH session
|
echo "📊 Tailscale 狀態:"
|
||||||
export REMOTE_WORKDIR="${{ env.REMOTE_WORKDIR }}"
|
tailscale status
|
||||||
export PROJECT_NAME="${{ env.PROJECT_NAME }}"
|
|
||||||
export IMAGE_NAME="${{ env.DOCKER_IMAGE_REPO }}:${{ env.IMAGE_TAG }}"
|
|
||||||
export DOCKERHUB_USERNAME="${{ secrets.DOCKERHUB_USERNAME }}"
|
|
||||||
export DOCKERHUB_TOKEN="${{ secrets.DOCKERHUB_TOKEN }}"
|
|
||||||
export SSH_USER="${{ secrets.SSH_USER }}"
|
|
||||||
export SSH_HOST="${{ secrets.SSH_HOST }}"
|
|
||||||
|
|
||||||
# Create remote script
|
# 顯示本機 Tailscale IP
|
||||||
cat > /tmp/remote_build.sh << 'SCRIPT_EOF'
|
echo ""
|
||||||
#!/bin/bash
|
echo "🌐 本機 Tailscale IP:"
|
||||||
set -e
|
tailscale ip -4
|
||||||
|
|
||||||
REMOTE_WORKDIR="$1"
|
# 等待 Tailscale 完全連線
|
||||||
PROJECT_NAME="$2"
|
echo ""
|
||||||
IMAGE_NAME="$3"
|
echo "⏳ 等待 Tailscale 連線穩定..."
|
||||||
DOCKERHUB_USERNAME="$4"
|
sleep 5
|
||||||
DOCKERHUB_TOKEN="$5"
|
|
||||||
|
|
||||||
# Step 1: Enter work directory
|
# 測試與目標主機的連通性
|
||||||
cd "${REMOTE_WORKDIR}"
|
echo ""
|
||||||
|
echo "📡 測試與遠端主機的連通性..."
|
||||||
|
echo "目標主機: ${{ secrets.SSH_HOST }}"
|
||||||
|
|
||||||
# Step 2: Clone or update project
|
if tailscale ping --c 3 "${{ secrets.SSH_HOST }}"; then
|
||||||
if [ ! -d "${PROJECT_NAME}" ]; then
|
echo ""
|
||||||
git clone https://github.com/pi-docket/ConvertX-CN.git
|
echo "✅ Tailscale 連線測試成功!"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ Tailscale ping 失敗,但 SSH 可能仍可運作,繼續執行..."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd "${PROJECT_NAME}"
|
echo "========================================"
|
||||||
git pull
|
|
||||||
|
|
||||||
echo "Building image: ${IMAGE_NAME}"
|
# ========================================
|
||||||
|
# Step 4: 測試 Tailscale SSH 連線
|
||||||
|
# 說明:使用 tailscale ssh 測試連線(不使用傳統 ssh)
|
||||||
|
# ========================================
|
||||||
|
- name: 🔐 測試 Tailscale SSH 連線
|
||||||
|
run: |
|
||||||
|
echo "========================================"
|
||||||
|
echo "🔐 測試 Tailscale SSH 連線"
|
||||||
|
echo "========================================"
|
||||||
|
echo "使用者: ${{ secrets.SSH_USER }}"
|
||||||
|
echo "主機: ${{ secrets.SSH_HOST }}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
# Step 3: Docker Login (must use Action PAT)
|
# 使用 tailscale ssh 連線測試
|
||||||
echo "${DOCKERHUB_TOKEN}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin
|
tailscale ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} \
|
||||||
|
"echo '✅ Tailscale SSH 連線成功!' && echo '主機名稱:' && hostname && echo '當前使用者:' && whoami"
|
||||||
|
|
||||||
# Step 4: Docker Build & Push
|
echo ""
|
||||||
docker build -t "${IMAGE_NAME}" .
|
echo "========================================"
|
||||||
docker push "${IMAGE_NAME}"
|
|
||||||
|
|
||||||
echo "Successfully pushed: ${IMAGE_NAME}"
|
# ========================================
|
||||||
|
# Step 5: 準備遠端工作目錄
|
||||||
|
# 說明:確保遠端工作目錄存在
|
||||||
|
# ========================================
|
||||||
|
- name: 📁 準備遠端工作目錄
|
||||||
|
run: |
|
||||||
|
echo "========================================"
|
||||||
|
echo "📁 準備遠端工作目錄"
|
||||||
|
echo "========================================"
|
||||||
|
echo "工作目錄: ${{ env.REMOTE_WORKDIR }}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
# Step 5: Safe cleanup (only this build image)
|
tailscale ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} \
|
||||||
docker rmi "${IMAGE_NAME}" || true
|
"mkdir -p ${{ env.REMOTE_WORKDIR }} && echo '✅ 工作目錄已就緒'"
|
||||||
docker builder prune -f --filter "unused-for=24h"
|
|
||||||
|
|
||||||
# Logout for security
|
echo "========================================"
|
||||||
docker logout
|
|
||||||
|
|
||||||
echo "Cleanup completed"
|
# ========================================
|
||||||
SCRIPT_EOF
|
# Step 6: Clone 或更新專案程式碼
|
||||||
|
# 說明:在遠端主機上 clone 或 pull 最新程式碼
|
||||||
|
# ========================================
|
||||||
|
- name: 📥 Clone 或更新專案程式碼
|
||||||
|
run: |
|
||||||
|
echo "========================================"
|
||||||
|
echo "📥 Clone 或更新專案程式碼"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
chmod +x /tmp/remote_build.sh
|
tailscale ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'REMOTE_EOF'
|
||||||
|
set -e
|
||||||
|
|
||||||
# Copy script to remote and execute
|
WORKDIR="${{ env.REMOTE_WORKDIR }}"
|
||||||
scp -o StrictHostKeyChecking=no /tmp/remote_build.sh "${SSH_USER}@${SSH_HOST}:/tmp/remote_build.sh"
|
PROJECT="${{ env.PROJECT_NAME }}"
|
||||||
ssh -o StrictHostKeyChecking=no "${SSH_USER}@${SSH_HOST}" "bash /tmp/remote_build.sh '${REMOTE_WORKDIR}' '${PROJECT_NAME}' '${IMAGE_NAME}' '${DOCKERHUB_USERNAME}' '${DOCKERHUB_TOKEN}'"
|
REPO_URL="https://github.com/pi-docket/ConvertX-CN.git"
|
||||||
ssh -o StrictHostKeyChecking=no "${SSH_USER}@${SSH_HOST}" "rm -f /tmp/remote_build.sh"
|
|
||||||
|
|
||||||
- name: Cleanup SSH Key
|
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 7: 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 8: 設定 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 9: Multi-Arch Docker Build & Push
|
||||||
|
# 說明:使用 buildx 同時建構 amd64 和 arm64 並推送
|
||||||
|
# ========================================
|
||||||
|
- name: 🔨 Multi-Arch Docker Build & Push
|
||||||
|
run: |
|
||||||
|
echo "========================================"
|
||||||
|
echo "🔨 開始 Multi-Arch Docker Build & Push"
|
||||||
|
echo "========================================"
|
||||||
|
echo "映像名稱: ${{ env.DOCKER_IMAGE_REPO }}:${{ github.event.inputs.image_tag }}"
|
||||||
|
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 }}"
|
||||||
|
BUILDER_NAME="multiarch-builder-${{ github.run_id }}"
|
||||||
|
|
||||||
|
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}"
|
||||||
|
|
||||||
|
# Multi-arch build + push
|
||||||
|
docker buildx build \
|
||||||
|
--platform linux/amd64,linux/arm64 \
|
||||||
|
-t "${IMAGE}" \
|
||||||
|
--push \
|
||||||
|
--progress=plain \
|
||||||
|
.
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Multi-Arch Docker build & push 完成!"
|
||||||
|
echo ""
|
||||||
|
echo "📦 映像已推送至 Docker Hub:"
|
||||||
|
echo " ${IMAGE}"
|
||||||
|
echo ""
|
||||||
|
echo "🏗️ 支援的架構:"
|
||||||
|
echo " - linux/amd64"
|
||||||
|
echo " - linux/arm64"
|
||||||
|
REMOTE_EOF
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Step 10: 驗證 Docker Hub 映像
|
||||||
|
# 說明:確認映像已成功推送並檢查架構
|
||||||
|
# ========================================
|
||||||
|
- name: ✅ 驗證 Docker Hub 映像
|
||||||
|
run: |
|
||||||
|
echo "========================================"
|
||||||
|
echo "✅ 驗證 Docker Hub 映像"
|
||||||
|
echo "========================================"
|
||||||
|
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 11: 清理本次 Build 資源
|
||||||
|
# 說明:只清理本次 build 產生的 builder 和 cache
|
||||||
|
# ========================================
|
||||||
|
- name: 🧹 清理本次 Build 資源
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
rm -f ~/.ssh/id_rsa
|
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 12: 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 13: 顯示完成摘要
|
||||||
|
# 說明:顯示本次 build 的摘要資訊
|
||||||
|
# ========================================
|
||||||
|
- name: 📋 Build 完成摘要
|
||||||
|
run: |
|
||||||
|
echo "========================================"
|
||||||
|
echo "✅ Multi-Arch Docker Build & Push 完成!"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
echo "📦 映像資訊:"
|
||||||
|
echo " - 映像名稱: ${{ env.DOCKER_IMAGE_REPO }}:${{ github.event.inputs.image_tag }}"
|
||||||
|
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 }}"
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "🎉 所有步驟已成功完成!"
|
||||||
|
echo "========================================"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue