Skip to content

Publish: review-model → main #731

Publish: review-model → main

Publish: review-model → main #731

name: Sync Model CN to Aliyun OSS
on:
pull_request:
types:
- closed
branches:
- main
paths:
- 'model/cn/**'
workflow_dispatch:
inputs:
manual_run:
description: '手动触发全量同步 model/cn 数据到 OSS'
required: false
default: 'true'
jobs:
sync:
runs-on: ubuntu-latest
# PR 触发时仅在合并后运行,手动触发时始终运行
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true)
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect changed model/cn files
id: changes
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "手动触发:全量同步模式"
echo "SYNC_MODE=full" >> "$GITHUB_ENV"
echo "sync_mode=full" >> "$GITHUB_OUTPUT"
else
echo "PR 合并触发:增量同步模式"
echo "SYNC_MODE=incremental" >> "$GITHUB_ENV"
echo "sync_mode=incremental" >> "$GITHUB_OUTPUT"
# 获取 PR 中 model/cn/ 下变更的文件
CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- 'model/cn/' || true)
DELETED_FILES=$(git diff --name-only --diff-filter=D "$BASE_SHA" "$HEAD_SHA" -- 'model/cn/' || true)
ADDED_MODIFIED_FILES=$(git diff --name-only --diff-filter=d "$BASE_SHA" "$HEAD_SHA" -- 'model/cn/' || true)
if [ -z "$CHANGED_FILES" ]; then
echo "没有 model/cn/ 下的文件变更,跳过同步"
echo "HAS_CHANGES=false" >> "$GITHUB_ENV"
echo "has_changes=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "变更的文件:"
echo "$CHANGED_FILES"
# 保存文件列表供后续步骤使用
echo "$ADDED_MODIFIED_FILES" > /tmp/changed_model_files.txt
echo "$DELETED_FILES" > /tmp/deleted_model_files.txt
echo "HAS_CHANGES=true" >> "$GITHUB_ENV"
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Validate secrets
if: env.SYNC_MODE == 'full' || env.HAS_CHANGES == 'true'
env:
OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
OSS_ENDPOINT: ${{ secrets.OSS_ENDPOINT }}
OSS_BUCKET: ${{ secrets.OSS_BUCKET }}
run: |
set -euo pipefail
echo "Validating required secrets..."
for var in OSS_ACCESS_KEY_ID OSS_ACCESS_KEY_SECRET OSS_ENDPOINT OSS_BUCKET; do
if [ -z "${!var}" ]; then
echo "ERROR: $var is not set"
exit 1
fi
done
echo "All required secrets are present"
- name: Setup ossutil
if: env.SYNC_MODE == 'full' || env.HAS_CHANGES == 'true'
run: |
set -euo pipefail
wget -q https://gosspublic.alicdn.com/ossutil/1.7.15/ossutil64
chmod +x ossutil64
sudo mv ossutil64 /usr/local/bin/ossutil
ossutil --version
- name: Configure ossutil
if: env.SYNC_MODE == 'full' || env.HAS_CHANGES == 'true'
env:
OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
OSS_ENDPOINT: ${{ secrets.OSS_ENDPOINT }}
run: |
set -euo pipefail
ossutil config -e $OSS_ENDPOINT -i $OSS_ACCESS_KEY_ID -k $OSS_ACCESS_KEY_SECRET
ossutil ls oss:// || echo "Warning: Could not list OSS buckets"
- name: Extract resource URLs and download files
if: env.SYNC_MODE == 'full' || env.HAS_CHANGES == 'true'
run: |
set -euo pipefail
mkdir -p downloads
DOWNLOAD_COUNT=0
FAIL_COUNT=0
echo "========================================="
echo "Starting resource file download process"
echo "========================================="
DOWNLOAD_EXTENSIONS='\.(png|jpg|jpeg|gif|webp|bmp|svg|ico|3mf|stl|gcode|snap3dp|snapcnc|snaplaser|zip|tar|gz|tgz|rar|7z|pdf|mp3|mp4|wav|avi|mov|exe|bin|apk|dmg|deb|rpm|msi|iso)$'
ALL_URLS_FILE="all_resource_urls.txt"
> "$ALL_URLS_FILE"
if [ "$SYNC_MODE" = "incremental" ]; then
# 增量模式:仅从变更的文件中提取 URL
echo "增量模式:仅处理变更文件的资源"
while IFS= read -r changed_file; do
[ -z "$changed_file" ] && continue
if [ -f "$changed_file" ]; then
echo "Extracting URLs from: $changed_file"
grep -oE 'https?://[^"'\''[:space:]<>]+' "$changed_file" 2>/dev/null >> "$ALL_URLS_FILE" || true
fi
done < /tmp/changed_model_files.txt
else
# 全量模式:处理所有文件
while IFS= read -r -d '' html_file; do
echo "Extracting URLs from: $html_file"
grep -oE 'https?://[^"'\''[:space:]<>]+' "$html_file" 2>/dev/null >> "$ALL_URLS_FILE" || true
done < <(find model/cn/ -name "*.html" -type f -print0 2>/dev/null)
fi
SORTED_URLS_FILE="sorted_resource_urls.txt"
sort -u "$ALL_URLS_FILE" > "$SORTED_URLS_FILE"
rm -f "$ALL_URLS_FILE"
while IFS= read -r url; do
[ -z "$url" ] && continue
url=$(echo "$url" | sed 's/[,;:]*$//')
url_main=$(echo "$url" | cut -d'?' -f1)
if ! echo "$url_main" | grep -qiE "$DOWNLOAD_EXTENSIONS"; then
continue
fi
url_without_protocol=$(echo "$url" | sed 's|https\?://||')
url_path=$(echo "$url_without_protocol" | cut -d'/' -f2-)
target_dir="downloads/download/$(dirname "$url_path")"
target_file="downloads/download/$url_path"
mkdir -p "$target_dir"
if wget -q --timeout=60 --tries=3 -O "$target_file" "$url"; then
DOWNLOAD_COUNT=$((DOWNLOAD_COUNT + 1))
echo " [下载] $url_path"
else
echo "DOWNLOAD_FAILED: $url" >> sync_errors.txt
rm -f "$target_file"
FAIL_COUNT=$((FAIL_COUNT + 1))
echo " [失败] $url"
fi
done < "$SORTED_URLS_FILE"
rm -f "$SORTED_URLS_FILE"
echo "DOWNLOAD_COUNT=$DOWNLOAD_COUNT" >> "$GITHUB_ENV"
echo "FAIL_COUNT=$FAIL_COUNT" >> "$GITHUB_ENV"
echo "========================================="
echo "下载完成: ${DOWNLOAD_COUNT} 个, 失败 ${FAIL_COUNT} 个"
echo "========================================="
- name: Create modified copies and inject missing JSON data
if: env.SYNC_MODE == 'full' || env.HAS_CHANGES == 'true'
env:
CDN_DOMAIN: ${{ secrets.CDN_DOMAIN }}
run: |
set -euo pipefail
rm -rf temp_oss
mkdir -p temp_oss/model
EXTENSIONS="png|jpg|jpeg|gif|webp|bmp|svg|ico|3mf|stl|gcode|snap3dp|snapcnc|snaplaser|zip|tar|gz|tgz|rar|7z|pdf|mp3|mp4|wav|avi|mov|exe|bin|apk|dmg|deb|rpm|msi|iso"
if [ "$SYNC_MODE" = "incremental" ]; then
# 增量模式:仅复制和处理变更的文件
echo "增量模式:仅处理变更的文件"
while IFS= read -r changed_file; do
[ -z "$changed_file" ] && continue
if [ -f "$changed_file" ]; then
# changed_file 格式: model/cn/xxx.html -> temp_oss/model/xxx.html
rel_path="${changed_file#model/cn/}"
target_dir="temp_oss/model/$(dirname "$rel_path")"
mkdir -p "$target_dir"
cp -a "$changed_file" "temp_oss/model/$rel_path"
fi
done < /tmp/changed_model_files.txt
else
# 全量模式:复制所有文件
if [ ! -d "model/cn" ] || [ -z "$(ls -A model/cn 2>/dev/null)" ]; then
echo "ERROR: model/cn directory is empty or not found"
exit 1
fi
cp -a model/cn/* temp_oss/model/
fi
# 去掉 .html 扩展名
while IFS= read -r -d '' f; do
mv "$f" "${f%.html}"
done < <(find temp_oss -type f -name "*.html" -print0)
echo "Applying URL replacements..."
while IFS= read -r -d '' html_file; do
sed -i -E "s#https://[a-zA-Z0-9.-]+/([^\"]+\.($EXTENSIONS))#https://$CDN_DOMAIN/download/\1#gi" "$html_file"
sed -i -E "s#https://[a-zA-Z0-9.-]+/api/model/detail/([0-9]+)#https://$CDN_DOMAIN/model/detail/\1#g" "$html_file"
sed -i 's/snapmaker\.com/snapmaker.cn/g' "$html_file"
done < <(find temp_oss -type f -print0)
- name: Sync model files to OSS (incremental)
if: env.SYNC_MODE == 'incremental' && env.HAS_CHANGES == 'true'
env:
OSS_BUCKET: ${{ secrets.OSS_BUCKET }}
run: |
set -euo pipefail
MODEL_UPLOAD_COUNT=0
MODEL_DELETE_COUNT=0
# 上传变更的文件
if [ -d "temp_oss/model" ] && [ -n "$(ls -A temp_oss/model 2>/dev/null)" ]; then
echo "增量上传变更的 model 文件..."
while IFS= read -r -d '' file; do
rel_path=${file#temp_oss/}
ossutil cp -f "$file" "oss://$OSS_BUCKET/$rel_path" \
--meta "Content-Type:application/json#Cache-Control:public, max-age=3600#Content-Disposition:inline"
MODEL_UPLOAD_COUNT=$((MODEL_UPLOAD_COUNT + 1))
echo " [上传] $rel_path"
done < <(find temp_oss/model -type f -print0)
fi
# 删除在 PR 中被删除的文件
if [ -f /tmp/deleted_model_files.txt ]; then
while IFS= read -r deleted_file; do
[ -z "$deleted_file" ] && continue
# deleted_file 格式: model/cn/detail/xx.html -> OSS key: model/detail/xx (去掉 cn/ 和 .html)
rel_path="${deleted_file#model/cn/}"
oss_key="model/${rel_path%.html}"
echo " [删除] oss://$OSS_BUCKET/$oss_key"
ossutil rm -f "oss://$OSS_BUCKET/$oss_key" || true
MODEL_DELETE_COUNT=$((MODEL_DELETE_COUNT + 1))
done < /tmp/deleted_model_files.txt
fi
echo "MODEL_UPLOAD_COUNT=$MODEL_UPLOAD_COUNT" >> "$GITHUB_ENV"
echo "MODEL_DELETE_COUNT=$MODEL_DELETE_COUNT" >> "$GITHUB_ENV"
echo "增量同步完成: 上传 $MODEL_UPLOAD_COUNT 个, 删除 $MODEL_DELETE_COUNT 个"
- name: Sync model files to OSS (full) and delete stale files
if: env.SYNC_MODE == 'full'
env:
OSS_BUCKET: ${{ secrets.OSS_BUCKET }}
run: |
set -euo pipefail
MODEL_UPLOAD_COUNT=0
MODEL_DELETE_COUNT=0
if [ ! -d "temp_oss/model" ] || [ -z "$(ls -A temp_oss/model 2>/dev/null)" ]; then
echo "No model files to sync"
else
echo "全量同步 model 文件到 OSS..."
while IFS= read -r -d '' file; do
rel_path=${file#temp_oss/}
ossutil cp -f "$file" "oss://$OSS_BUCKET/$rel_path" \
--meta "Content-Type:application/json#Cache-Control:public, max-age=3600#Content-Disposition:inline"
MODEL_UPLOAD_COUNT=$((MODEL_UPLOAD_COUNT + 1))
done < <(find temp_oss/model -type f -print0)
# 构建本地 key 集合
LOCAL_MODEL_KEYS="local_model_keys.txt"
> "$LOCAL_MODEL_KEYS"
while IFS= read -r -d '' file; do
echo "${file#temp_oss/}" >> "$LOCAL_MODEL_KEYS"
done < <(find temp_oss/model -type f -print0)
# 删除 OSS 上多余的文件
OSS_MODEL_KEYS="oss_model_keys.txt"
ossutil ls "oss://$OSS_BUCKET/model/" -s 2>/dev/null \
| grep -v '^$' | sed "s|oss://$OSS_BUCKET/||" > "$OSS_MODEL_KEYS" || true
while IFS= read -r oss_key; do
[ -z "$oss_key" ] && continue
if ! grep -qxF "$oss_key" "$LOCAL_MODEL_KEYS"; then
echo " [删除] oss://$OSS_BUCKET/$oss_key"
ossutil rm -f "oss://$OSS_BUCKET/$oss_key"
MODEL_DELETE_COUNT=$((MODEL_DELETE_COUNT + 1))
fi
done < "$OSS_MODEL_KEYS"
rm -f "$LOCAL_MODEL_KEYS" "$OSS_MODEL_KEYS"
echo "全量同步完成: 上传 $MODEL_UPLOAD_COUNT 个, 删除 $MODEL_DELETE_COUNT 个"
fi
echo "MODEL_UPLOAD_COUNT=$MODEL_UPLOAD_COUNT" >> "$GITHUB_ENV"
echo "MODEL_DELETE_COUNT=$MODEL_DELETE_COUNT" >> "$GITHUB_ENV"
- name: Sync downloaded resource files to OSS
if: env.SYNC_MODE == 'full' || env.HAS_CHANGES == 'true'
env:
OSS_BUCKET: ${{ secrets.OSS_BUCKET }}
run: |
set -euo pipefail
RESOURCE_UPLOAD_COUNT=0
if [ -d "downloads" ] && [ -n "$(ls -A downloads 2>/dev/null)" ]; then
echo "Syncing downloaded resource files to OSS..."
while IFS= read -r -d '' file; do
rel_path=${file#downloads/}
ossutil cp -f "$file" "oss://$OSS_BUCKET/$rel_path" \
--meta "Cache-Control:public, max-age=86400"
RESOURCE_UPLOAD_COUNT=$((RESOURCE_UPLOAD_COUNT + 1))
done < <(find downloads/ -type f -print0)
echo "资源文件上传完成: $RESOURCE_UPLOAD_COUNT 个"
fi
echo "RESOURCE_UPLOAD_COUNT=$RESOURCE_UPLOAD_COUNT" >> "$GITHUB_ENV"
- name: Cleanup temporary files
if: always()
run: |
rm -rf temp_oss downloads
rm -f sync_errors.txt all_resource_urls.txt sorted_resource_urls.txt
rm -f local_model_keys.txt oss_model_keys.txt
rm -f /tmp/changed_model_files.txt /tmp/deleted_model_files.txt
- name: Setup aliyun-cli for CDN
if: success() && (env.SYNC_MODE == 'full' || env.HAS_CHANGES == 'true')
run: |
set -euo pipefail
wget -q https://aliyuncli.alicdn.com/aliyun-cli-linux-latest-amd64.tgz
tar -xzf aliyun-cli-linux-latest-amd64.tgz
sudo mv aliyun /usr/local/bin/
rm -f aliyun-cli-linux-latest-amd64.tgz
- name: Refresh CDN cache
if: success() && (env.SYNC_MODE == 'full' || env.HAS_CHANGES == 'true')
env:
CDN_ACCESS_KEY_ID: ${{ secrets.CDN_ACCESS_KEY_ID }}
CDN_ACCESS_KEY_SECRET: ${{ secrets.CDN_ACCESS_KEY_SECRET }}
CDN_DOMAIN: ${{ secrets.CDN_DOMAIN }}
run: |
set -euo pipefail
if [ -z "${CDN_ACCESS_KEY_ID:-}" ] || [ -z "${CDN_ACCESS_KEY_SECRET:-}" ]; then
echo "CDN secrets not set, skipping CDN refresh"
exit 0
fi
aliyun configure set \
--profile cdn-profile \
--mode AK \
--region cn-hangzhou \
--access-key-id "$CDN_ACCESS_KEY_ID" \
--access-key-secret "$CDN_ACCESS_KEY_SECRET"
if [ "$SYNC_MODE" = "incremental" ]; then
# 增量模式:仅刷新变更文件对应的 CDN 路径
echo "增量刷新 CDN 缓存..."
REFRESH_URLS=""
# 刷新变更的 model 文件
if [ -d "temp_oss/model" ]; then
while IFS= read -r -d '' file; do
rel_path=${file#temp_oss/}
REFRESH_URLS="${REFRESH_URLS}https://$CDN_DOMAIN/$rel_path
"
done < <(find temp_oss -type f -print0 2>/dev/null)
fi
# 刷新被删除的文件
if [ -f /tmp/deleted_model_files.txt ]; then
while IFS= read -r deleted_file; do
[ -z "$deleted_file" ] && continue
rel_path="${deleted_file#model/cn/}"
oss_key="model/${rel_path%.html}"
REFRESH_URLS="${REFRESH_URLS}https://$CDN_DOMAIN/$oss_key
"
done < /tmp/deleted_model_files.txt
fi
if [ -n "$REFRESH_URLS" ]; then
aliyun --profile cdn-profile cdn RefreshObjectCaches \
--ObjectPath "$REFRESH_URLS" \
--ObjectType File 2>&1 || true
fi
# 资源文件目录刷新
if [ -d "downloads" ] && [ -n "$(ls -A downloads 2>/dev/null)" ]; then
aliyun --profile cdn-profile cdn RefreshObjectCaches \
--ObjectPath "https://$CDN_DOMAIN/download/" \
--ObjectType Directory 2>&1 || true
fi
else
# 全量模式:刷新整个目录
REFRESH_PATHS="https://$CDN_DOMAIN/model/
https://$CDN_DOMAIN/download/"
aliyun --profile cdn-profile cdn RefreshObjectCaches \
--ObjectPath "$REFRESH_PATHS" \
--ObjectType Directory 2>&1 || true
fi
- name: Job summary
if: always()
run: |
echo "========================================="
echo "同步完成汇总"
echo "========================================="
echo "同步模式: ${SYNC_MODE:-unknown}"
echo "资源文件: 下载 ${DOWNLOAD_COUNT:-0} 个, 失败 ${FAIL_COUNT:-0} 个"
echo "Model 文件: 上传 ${MODEL_UPLOAD_COUNT:-0} 个, 删除 ${MODEL_DELETE_COUNT:-0} 个"
echo "资源文件上传: ${RESOURCE_UPLOAD_COUNT:-0} 个"
if [ -f sync_errors.txt ] && [ -s sync_errors.txt ]; then
echo ""
echo "警告:以下文件下载失败:"
cat sync_errors.txt
fi