Skip to content

perf(cdn): 放宽源站限流并新增 OSS 源站同步以加速预热 #13

perf(cdn): 放宽源站限流并新增 OSS 源站同步以加速预热

perf(cdn): 放宽源站限流并新增 OSS 源站同步以加速预热 #13

Workflow file for this run

name: Release Electron App

Check failure on line 1 in .github/workflows/release.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/release.yml

Invalid workflow file

(Line: 163, Col: 13): Unrecognized named-value: 'secrets'. Located at position 1 within expression: secrets.OSS_BUCKET != '' && secrets.ALIYUN_ACCESS_KEY_ID != '', (Line: 212, Col: 13): Unrecognized named-value: 'secrets'. Located at position 1 within expression: secrets.DOWNLOAD_BASE_URL != '' && secrets.ALIYUN_ACCESS_KEY_ID != ''
# 由 semantic-release 创建的 v* tag 触发:构建 Electron 安装包并附加到对应 Release
on:
push:
tags: ['v*']
# 兜底触发:semantic-release 若以内置 GITHUB_TOKEN 推 tag,
# GitHub 防递归机制不会派发 push 事件,导致本工作流从不运行、
# Release 资产为空(latest.yml 404)。Release 发布事件可正常触发补跑。
release:
types: [published]
# 手动兜底:在 UI 选择对应 tag 运行,用于补发历史版本资产
workflow_dispatch:
# 上传构建产物到 GitHub Release 需要写权限
permissions:
contents: write
# 同一 tag 的多个触发源(push/release)串行执行,避免重复上传竞争
concurrency:
group: release-electron-${{ github.ref_name }}
cancel-in-progress: false
jobs:
lint-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: client/package-lock.json
- run: cd client && npm ci
- run: cd client && npm run lint
- run: cd client && npm run test -- --run
build:
needs: lint-test
# 本产品为 Windows-only(electron-builder.yml 仅定义 win target)。
# macOS 无有效构建目标必然报 ERR_ELECTRON_BUILDER_CANNOT_EXECUTE,
# 且默认 fail-fast 会连带取消 Windows 构建,导致 release/同步步骤永不执行。
# 如未来支持 mac,需补回带签名的 mac target 后再加入 matrix。
strategy:
matrix:
os: [windows-latest]
runs-on: ${{ matrix.os }}
steps:
# lfs: true 必需:app-icon.png 等 PNG 由 Git LFS 托管(.gitattributes:
# *.png filter=lfs),不拉取则仅为 131 字节指针文件,
# electron-builder 图标转换(PNG→ICO)解码失败,
# 报 ERR_ELECTRON_BUILDER_CANNOT_EXECUTE。
- uses: actions/checkout@v4
with:
lfs: true
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: client/package-lock.json
- run: cd client && npm ci
# --publish never:发布由下方 release job 统一负责,避免 electron-builder
# 因检测到 tag 而自行尝试发布(github provider 无 GH_TOKEN 会报错)
#
# DOWNLOAD_BASE_URL 已配置时,用 -c.publish.0.url 覆盖 generic 更新源为 CDN,
# 使新包内的 app-update.yml 指向 CDN;未配置时沿用 electron-builder.yml
# 中的自建服务器地址。注:存量客户端的更新源已固化在已安装的
# app-update.yml 中,故 ECS 与 GitHub 两侧资产均需继续发布。
- name: Build Electron app
shell: bash
env:
DOWNLOAD_BASE_URL: ${{ secrets.DOWNLOAD_BASE_URL }}
run: |
cd client
if [ -n "$DOWNLOAD_BASE_URL" ]; then
echo "Update feed -> $DOWNLOAD_BASE_URL"
npm run electron:build -- --publish never -c.publish.0.url="$DOWNLOAD_BASE_URL"
else
echo "Update feed -> 默认(electron-builder.yml 中的自建服务器)"
npm run electron:build -- --publish never
fi
- uses: actions/upload-artifact@v4
with:
name: release-${{ matrix.os }}
# latest*.yml 与 *.blockmap 是 electron-updater 自动更新的必需文件
path: |
client/release/*.exe
client/release/*.blockmap
client/release/latest*.yml
release:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
pattern: release-*
- uses: softprops/action-gh-release@v2
with:
files: release-*/*
# ---------------------------------------------------------------
# 同步 Windows 安装包至自建服务器(国内主下载源 + 自动更新源)
# latest.json 供官网下载页动态展示版本/直链;latest.yml 供 electron-updater
# ---------------------------------------------------------------
- name: Generate latest.json for website
id: meta
run: |
YML=release-windows-latest/latest.yml
VERSION=$(grep -m1 '^version:' "$YML" | awk '{print $2}')
FILE=$(grep -m1 '^path:' "$YML" | sed 's/^path: //')
SIZE=$(stat -c%s "release-windows-latest/$FILE")
SHA256=$(sha256sum "release-windows-latest/$FILE" | awk '{print $1}')
DATE=$(date -u +%F)
printf '{"version":"%s","fileName":"%s","size":%s,"sha256":"%s","releaseDate":"%s"}\n' \
"$VERSION" "$FILE" "$SIZE" "$SHA256" "$DATE" > release-windows-latest/latest.json
cat release-windows-latest/latest.json
# 供下方 CDN 预热步骤拼接 URL
echo "file=$FILE" >> "$GITHUB_OUTPUT"
- name: Ensure downloads directory on server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: mkdir -p /opt/Entropydecrease/downloads
# 先传二进制(exe/blockmap),后传元数据(yml/json):
# 避免 latest.yml 率先就位时指向尚未上传完成的安装包(更新 404 窗口期)
- name: Upload installer binaries to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: release-windows-latest/*.exe,release-windows-latest/*.blockmap
target: /opt/Entropydecrease/downloads
strip_components: 1
- name: Publish update metadata (last, avoids 404 window)
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: release-windows-latest/latest.yml,release-windows-latest/latest.json
target: /opt/Entropydecrease/downloads
strip_components: 1
# ---------------------------------------------------------------
# 同步至 OSS(CDN 源站)
# 为何需要 OSS:ECS 出网仅约 6Mbps,而 CDN 预热需**每个边缘节点各
# 拉一份完整文件**,回源总量 = 节点数 x 172MB,以 ECS 为源站时
# 预热耗时达几十分钟至数小时且会打满带宽影响 API。
# OSS 出网带宽极大,可使预热降至分钟级且完全不占用 ECS。
#
# 路径与 ECS 保持一致(downloads/ 前缀),因此无需改变
# DOWNLOAD_BASE_URL,也无需改前端与 electron-updater 配置。
# ECS 同步仍保留:存量客户端的更新源仍指向 ECS,且作为兜底。
# ---------------------------------------------------------------
- name: Upload to OSS (CDN origin)
if: ${{ secrets.OSS_BUCKET != '' && secrets.ALIYUN_ACCESS_KEY_ID != '' }}
env:
AK_ID: ${{ secrets.ALIYUN_ACCESS_KEY_ID }}
AK_SECRET: ${{ secrets.ALIYUN_ACCESS_KEY_SECRET }}
OSS_BUCKET: ${{ secrets.OSS_BUCKET }}
# 未配置时默认杭州(与 ECS 同地域)
OSS_ENDPOINT: ${{ secrets.OSS_ENDPOINT || 'oss-cn-hangzhou.aliyuncs.com' }}
FILE_NAME: ${{ steps.meta.outputs.file }}
run: |
set -eo pipefail
curl -sL https://aliyuncli.alicdn.com/aliyun-cli-linux-latest-amd64.tgz -o /tmp/aliyun.tgz
tar -xzf /tmp/aliyun.tgz -C /tmp
oss_cp() {
/tmp/aliyun oss cp "$1" "oss://$OSS_BUCKET/downloads/$2" \
--force -e "$OSS_ENDPOINT" -i "$AK_ID" -k "$AK_SECRET"
}
cd release-windows-latest
# 同 ECS 的顺序约定:先二进制后元数据,避免 latest.yml 率先就位
# 而指向尚未上传完成的安装包
oss_cp "$FILE_NAME" "$FILE_NAME"
oss_cp "$FILE_NAME.blockmap" "$FILE_NAME.blockmap"
oss_cp latest.yml latest.yml
oss_cp latest.json latest.json
echo "OSS sync done: oss://$OSS_BUCKET/downloads/"
# 保留策略:仅保留最近 3 个版本的安装包,防止磁盘增长
- name: Prune old versions (keep last 3)
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /opt/Entropydecrease/downloads
ls -t -- *.exe 2>/dev/null | tail -n +4 | while read -r f; do
rm -f -- "$f" "$f.blockmap"
done
# ---------------------------------------------------------------
# CDN 预热:大文件在 CDN 上按分片缓存,未被请求过的分片不在缓存中,
# 首批用户会退化为实时回源而受源站带宽限制。
# 实测:已缓存分片 10.5MB/s,未缓存分片仅 106KB/s(相差约 95 倍),
# 故预热不是可选优化而是大文件分发的必要步骤。
# 预热为异步任务,API 返回后后台继续拉取,此处不等待完成。
# ---------------------------------------------------------------
- name: Prefetch CDN cache
# 仅在已切换至 CDN 且配置了预热凭证时执行;未配置则跳过
if: ${{ secrets.DOWNLOAD_BASE_URL != '' && secrets.ALIYUN_ACCESS_KEY_ID != '' }}
# 预热失败不应推翻已成功的发布,仅标记警告
continue-on-error: true
env:
ALIBABA_CLOUD_ACCESS_KEY_ID: ${{ secrets.ALIYUN_ACCESS_KEY_ID }}
ALIBABA_CLOUD_ACCESS_KEY_SECRET: ${{ secrets.ALIYUN_ACCESS_KEY_SECRET }}
DOWNLOAD_BASE_URL: ${{ secrets.DOWNLOAD_BASE_URL }}
FILE_NAME: ${{ steps.meta.outputs.file }}
run: |
set -eo pipefail
curl -sL https://aliyuncli.alicdn.com/aliyun-cli-linux-latest-amd64.tgz -o /tmp/aliyun.tgz
tar -xzf /tmp/aliyun.tgz -C /tmp
BASE="${DOWNLOAD_BASE_URL%/}"
# 预热安装包与 blockmap(差量更新依赖);
# latest.yml/json 按设计不缓存,且仅几百字节,无需预热
OBJECTS="$BASE/$FILE_NAME"$'\n'"$BASE/$FILE_NAME.blockmap"
echo "Prefetching:"
echo "$OBJECTS"
/tmp/aliyun cdn PushObjectCache \
--region cn-hangzhou \
--ObjectPath "$OBJECTS" \
--Area domestic