name: Release on: push: tags: - "v*" permissions: contents: write packages: write # 允许推送 Docker 镜像到 ghcr.io(build-server-docker job) actions: write # 允许在 release job 末尾删除旧 artifact(避免 500MB 配额耗尽) jobs: # ── 前置 Job: 检测上一个版本 tag → 当前 tag 之间哪些组件目录有变更 ── # 规则: # fluxDown/** → 浏览器扩展(extension) # web/**、native/server/** → headless 服务器(server,Docker 镜像 + 多平台二进制) # native/engine/** → 桌面客户端 + 服务器 + 移动端(共享下载引擎) # docker/** → 服务器(部署文件变更需重发镜像) # android/**、lib/src/mobile/** → 移动端资产(Android APK) # website/** → 官网(独立走 deploy-website.yml 部署,不发 release) # docs/**、*.md → 纯文档,不触发任何构建 # 其余任何路径 → 桌面客户端(app) # 找不到上一个 v* tag(首个版本)时全部视为有变更。 changes: name: Detect Changed Components runs-on: ubuntu-latest outputs: app: ${{ steps.detect.outputs.app }} extension: ${{ steps.detect.outputs.extension }} server: ${{ steps.detect.outputs.server }} mobile: ${{ steps.detect.outputs.mobile }} cli: ${{ steps.detect.outputs.cli }} steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true - name: Detect changes since previous version tag id: detect shell: bash run: | TAG="${GITHUB_REF#refs/tags/}" # 只匹配 v0.1.2 形式的版本 tag(glob 从头锚定, # 自动忽略 release 流程创建的 extension-v* / website-v* 组件 tag) PREV=$(git describe --tags --abbrev=0 --match 'v[0-9]*' "${TAG}^" 2>/dev/null || true) APP=false; EXTENSION=false; SERVER=false; MOBILE=false; CLI=false if [ -z "$PREV" ]; then echo "No previous version tag found — building everything" APP=true; EXTENSION=true; SERVER=true; MOBILE=true; CLI=true else echo "Diff range: ${PREV}..${TAG}" CHANGED=$(git diff --name-only "${PREV}" "${TAG}") echo "── Changed files ──" echo "$CHANGED" while IFS= read -r f; do [ -z "$f" ] && continue case "$f" in fluxDown/*) EXTENSION=true ;; web/*) SERVER=true ;; native/server/*) SERVER=true ;; native/cli/*) CLI=true ;; native/api/*) SERVER=true; CLI=true ;; native/engine/*) APP=true; SERVER=true; MOBILE=true; CLI=true ;; docker/*) SERVER=true ;; android/*) MOBILE=true ;; lib/src/mobile/*) MOBILE=true ;; # 其余共享 Dart 代码(models/i18n/theme/bindings…)同时影响桌面端与移动端 lib/*) APP=true; MOBILE=true ;; pubspec.yaml|pubspec.lock) APP=true; MOBILE=true ;; website/*) ;; docs/*|*.md) ;; *) APP=true ;; esac done <<< "$CHANGED" fi { echo "app=$APP" echo "extension=$EXTENSION" echo "server=$SERVER" echo "mobile=$MOBILE" echo "cli=$CLI" } >> "$GITHUB_OUTPUT" echo "::notice::Components changed in ${PREV:-}..${TAG}: app=$APP extension=$EXTENSION server=$SERVER mobile=$MOBILE cli=$CLI" # ── 并行 Job 1: 构建 Windows 安装包(x64 + ARM64)── build-windows: name: Build Windows (${{ matrix.arch }}) needs: changes if: needs.changes.outputs.app == 'true' strategy: fail-fast: false matrix: include: - arch: x64 runner: windows-latest - arch: arm64 runner: windows-11-arm runs-on: ${{ matrix.runner }} steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Get version from tag id: version shell: bash run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" # ── Rust toolchain ── - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Cache Rust dependencies uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target key: rust-${{ runner.os }}-${{ matrix.arch }}-${{ hashFiles('**/Cargo.lock') }} restore-keys: | rust-${{ runner.os }}-${{ matrix.arch }}- - name: Cache Rinf CLI id: cache-rinf uses: actions/cache@v4 with: path: ~/.cargo/bin/rinf.exe key: rinf-cli-${{ runner.os }}-${{ matrix.arch }}-8.9.0 # ── Flutter SDK(x64 用预编译包,ARM64 用 git clone 原生安装)── - name: Setup Flutter (x64) if: matrix.arch == 'x64' uses: subosito/flutter-action@v2 with: channel: stable cache: true - name: Setup Flutter (ARM64 via git) if: matrix.arch == 'arm64' shell: bash run: | git clone https://github.com/flutter/flutter.git -b stable --depth 1 "$USERPROFILE/flutter" echo "$USERPROFILE/flutter/bin" >> "$GITHUB_PATH" - name: Cache Flutter (ARM64) if: matrix.arch == 'arm64' uses: actions/cache@v4 with: path: ~/flutter/bin/cache key: flutter-arm64-stable - name: Flutter precache & version info run: | flutter --version flutter precache --windows - name: Install Flutter dependencies run: flutter pub get # ── Rinf code generation ── - name: Install Rinf CLI if: steps.cache-rinf.outputs.cache-hit != 'true' shell: bash env: CARGO_HTTP_MULTIPLEXING: "false" CARGO_NET_RETRY: "10" run: | for i in 1 2 3; do cargo install rinf_cli@8.9.0 && exit 0 echo "cargo install failed (attempt $i), retrying in 15s..." sleep 15 done exit 1 - name: Generate Rinf bindings run: rinf gen # ── Build ── - name: Pre-build Rust hub (debug cargo output) shell: bash run: | cd native/hub echo "::group::Cargo build (cdylib) verbose output" cargo build --release --lib --verbose 2>&1 || echo "::error::Cargo build failed with exit code $?" echo "::endgroup::" - name: Build Flutter Windows app # Windows 使用 native-tls(Schannel),无需 aws-lc-sys,无需任何额外 TLS 环境变量 run: flutter build windows --release --build-name=${{ steps.version.outputs.VERSION }} --build-number=1 --dart-define=APP_VERSION=${{ steps.version.outputs.VERSION }} # ── ARM64: Flutter 在 ARM64 runner 上以 x64 仿真运行,输出到 build\windows\x64 # 但后续步骤(Inno Setup / portable zip)需要 build\windows\arm64 ── - name: Normalize build output path for ARM64 if: matrix.arch == 'arm64' shell: pwsh run: | if ((Test-Path "build\windows\x64") -and -not (Test-Path "build\windows\arm64")) { Rename-Item -Path "build\windows\x64" -NewName "arm64" Write-Host "Renamed build\windows\x64 -> build\windows\arm64" } # ── Inno Setup installer ── # NOTE: fluxdown_nmh.exe and fluxdown_updater.exe are built automatically # by CMake custom targets defined in windows/CMakeLists.txt during flutter build. - name: Ensure Inno Setup is installed if: matrix.arch == 'arm64' shell: pwsh run: | if (-not (Get-Command iscc -ErrorAction SilentlyContinue)) { choco install innosetup -y --no-progress echo "C:\Program Files (x86)\Inno Setup 6" | Out-File -FilePath $env:GITHUB_PATH -Append } # 语言文件已 vendor 进仓库(上游 issrc main 分支移除了 Unofficial 目录, # 原 raw.githubusercontent.com 下载地址 404),直接复制到 Inno Setup 目录 - name: Install Inno Setup Chinese Simplified language file shell: pwsh run: | $issLangDir = "C:\Program Files (x86)\Inno Setup 6\Languages" Copy-Item "installer\windows\ChineseSimplified.isl" -Destination "$issLangDir\ChineseSimplified.isl" -Force - name: Build installer with Inno Setup run: | iscc /DMyAppVersion=${{ steps.version.outputs.VERSION }} /DMyAppArch=${{ matrix.arch }} installer\windows\setup.iss # ── Compress portable version ── - name: Create portable zip shell: pwsh run: | New-Item -Path "build\windows\${{ matrix.arch }}\runner\Release\portable" -ItemType File -Force | Out-Null Compress-Archive -Path "build\windows\${{ matrix.arch }}\runner\Release\*" -DestinationPath "build\installer\FluxDown-${{ steps.version.outputs.VERSION }}-windows-${{ matrix.arch }}-portable.zip" # ── Upload artifacts(供 release job 使用)── - name: Upload installer artifact uses: actions/upload-artifact@v4 with: name: windows-installer-${{ matrix.arch }} path: build/installer/FluxDown-*.exe - name: Upload portable artifact uses: actions/upload-artifact@v4 with: name: windows-portable-${{ matrix.arch }} path: build/installer/FluxDown-*-portable.zip # ── 并行 Job 2: 构建浏览器扩展 ── build-extension: name: Build Browser Extension runs-on: ubuntu-latest needs: changes if: needs.changes.outputs.extension == 'true' steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "22" - name: Get version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" - name: Set extension version from tag working-directory: fluxDown run: | VERSION=${{ steps.version.outputs.VERSION }} sed -i "s/\"version\": \".*\"/\"version\": \"$VERSION\"/" package.json echo "Updated extension version to $VERSION" grep '"version"' package.json - name: Install dependencies run: npm ci working-directory: fluxDown - name: Build Chrome extension run: npm run build working-directory: fluxDown - name: Build Firefox extension run: npm run build:firefox working-directory: fluxDown - name: Package Chrome extension run: | VERSION=${{ steps.version.outputs.VERSION }} mkdir -p build/extension cd fluxDown/.output zip -r ../../build/extension/FluxDown-${VERSION}-chrome.zip chrome-mv3/ ls -la ../../build/extension/ - name: Install web-ext run: npm install -g web-ext # ── Firefox 双版本策略:AMO 市场用 X.Y.Z(与 tag 一致),GitHub Release XPI 用 X.Y.Z.1 # 两个版本号不同,彻底避免同版本号重复提交冲突。 # X.Y.Z → --channel=listed → AMO 公开市场,等待人工审核 # X.Y.Z.1 → --channel=unlisted → 立即签名,XPI 附到 GitHub Release 供离线安装 # Step 1: 提交 AMO 市场版本(X.Y.Z),continue-on-error 不阻塞 Release - name: Submit Firefox to AMO marketplace (listed, vX.Y.Z) continue-on-error: true run: | web-ext sign \ --source-dir fluxDown/.output/firefox-mv2 \ --channel=listed \ --api-key=${{ secrets.AMO_JWT_ISSUER }} \ --api-secret=${{ secrets.AMO_JWT_SECRET }} \ --approval-timeout=0 \ --artifacts-dir build/extension/firefox-listed/ # Step 2: 将 manifest 版本改为 X.Y.Z.1,unlisted 签名产出 GitHub Release 用 XPI # AMO 服务端有时返回 500,continue-on-error 避免阻断整个 Release - name: Sign Firefox extension for GitHub Release (unlisted, vX.Y.Z.1) continue-on-error: true run: | VERSION=${{ steps.version.outputs.VERSION }} mkdir -p build/extension python3 -c "import json;f='fluxDown/.output/firefox-mv2/manifest.json';m=json.load(open(f));m['version']='$VERSION.1';open(f,'w').write(json.dumps(m,indent=2))" echo "Manifest version set to $VERSION.1 for unlisted signing" web-ext sign \ --source-dir fluxDown/.output/firefox-mv2 \ --channel=unlisted \ --api-key=${{ secrets.AMO_JWT_ISSUER }} \ --api-secret=${{ secrets.AMO_JWT_SECRET }} \ --artifacts-dir build/extension/ 2>&1 | tee /tmp/amo_sign.log if [ "${PIPESTATUS[0]}" -ne 0 ]; then if grep -q "already exists" /tmp/amo_sign.log; then echo "::warning::Version $VERSION.1 already on AMO (CI re-run). Downloading existing XPI..." _amo_jwt() { local iat exp header payload sig iat=$(date +%s); exp=$((iat + 300)) header=$(printf '{"alg":"HS256","typ":"JWT"}' | base64 -w0 | tr '+/' '-_' | tr -d '=') payload=$(printf '{"iss":"%s","jti":"%s","iat":%s,"exp":%s}' \ "$AMO_ISSUER" "$iat" "$iat" "$exp" | base64 -w0 | tr '+/' '-_' | tr -d '=') sig=$(printf '%s.%s' "$header" "$payload" \ | openssl dgst -sha256 -hmac "$AMO_SECRET" -binary | base64 -w0 | tr '+/' '-_' | tr -d '=') printf '%s.%s.%s' "$header" "$payload" "$sig" } VERS_DATA=$(curl -sf -H "Authorization: JWT $(_amo_jwt)" \ "https://addons.mozilla.org/api/v5/addons/addon/fluxdown%40fluxdown.app/versions/?filter=all_with_unlisted") XPI_URL=$(echo "$VERS_DATA" | python3 -c "import sys,json;d=json.load(sys.stdin);v=next((x for x in(d.get('results')or[])if x.get('version')=='$VERSION.1'),None);sys.exit('not found') if not(v and(v.get('file')or{}).get('url')) else print(v['file']['url'])") curl -sf -L -H "Authorization: JWT $(_amo_jwt)" \ -o "build/extension/${VERSION}.xpi" "$XPI_URL" elif grep -q "was uploaded before and deleted" /tmp/amo_sign.log; then echo "::error::Version $VERSION.1 was deleted from AMO. Please use a new version number." exit 1 else cat /tmp/amo_sign.log exit 1 fi fi XPI_FILE=$(ls build/extension/*.xpi 2>/dev/null | head -1) if [ -n "$XPI_FILE" ]; then mv "$XPI_FILE" "build/extension/FluxDown-${VERSION}-firefox.xpi" echo "Firefox XPI ready: FluxDown-${VERSION}-firefox.xpi (internal version $VERSION.1)" else echo "::error::No signed XPI found." exit 1 fi env: AMO_ISSUER: ${{ secrets.AMO_JWT_ISSUER }} AMO_SECRET: ${{ secrets.AMO_JWT_SECRET }} VERSION: ${{ steps.version.outputs.VERSION }} # ── Upload artifacts(供 release job 使用)── - name: Upload Chrome extension artifact uses: actions/upload-artifact@v4 with: name: browser-extension-chrome path: build/extension/FluxDown-*-chrome.zip if-no-files-found: error # ── Chrome Web Store 自动发布(需 CHROME_CLIENT_ID / CHROME_CLIENT_SECRET / CHROME_REFRESH_TOKEN)── # ⚠️ Refresh Token 在 Google OAuth 同意屏幕处于 Testing 模式时 7 天过期。 # 发布前必须将同意屏幕切换到 Production,再重新走一次 OAuth 流程获取长期 token。 - name: Publish Chrome extension to Web Store continue-on-error: true env: CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }} CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }} CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }} run: | VERSION=${{ steps.version.outputs.VERSION }} ZIP_FILE="build/extension/FluxDown-${VERSION}-chrome.zip" EXTENSION_ID="meleenglfggcmcajknpeeeiobnpfmahc" # 用 refresh_token 换取新的 access_token ACCESS_TOKEN=$(curl -sf -X POST https://oauth2.googleapis.com/token \ -d "client_id=${CHROME_CLIENT_ID}" \ -d "client_secret=${CHROME_CLIENT_SECRET}" \ -d "refresh_token=${CHROME_REFRESH_TOKEN}" \ -d "grant_type=refresh_token" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])") # 上传新版本 zip echo "Uploading ${ZIP_FILE} to Chrome Web Store..." UPLOAD_RESP=$(curl -sf \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "x-goog-api-version: 2" \ -X PUT -T "${ZIP_FILE}" \ "https://www.googleapis.com/upload/chromewebstore/v1.1/items/${EXTENSION_ID}") echo "Upload response: ${UPLOAD_RESP}" UPLOAD_STATE=$(echo "${UPLOAD_RESP}" | python3 -c \ "import sys,json; print(json.load(sys.stdin).get('uploadState','UNKNOWN'))") if [ "${UPLOAD_STATE}" != "SUCCESS" ]; then echo "::error::Chrome Web Store upload failed (uploadState=${UPLOAD_STATE})" exit 1 fi # 发布已上传的草稿版本 PUBLISH_RESP=$(curl -sf \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "x-goog-api-version: 2" \ -H "Content-Length: 0" \ -X POST \ "https://www.googleapis.com/chromewebstore/v1.1/items/${EXTENSION_ID}/publish") echo "Publish response: ${PUBLISH_RESP}" STATUS=$(echo "${PUBLISH_RESP}" | python3 -c \ "import sys,json; print(','.join(json.load(sys.stdin).get('status',[])))") echo "Publish status codes: ${STATUS}" if echo "${STATUS}" | grep -qi "error"; then echo "::error::Chrome Web Store publish failed (status=${STATUS})" exit 1 fi echo "::notice::Chrome extension v${VERSION} submitted to Web Store review" # ── Edge Add-ons 商店自动发布(需 EDGE_CLIENT_ID / EDGE_API_KEY / EDGE_PRODUCT_ID)── # Edge 复用 Chrome MV3 构建(同为 MV3),但 Edge 商店拒绝 manifest 含 key 字段的包 # (PackageValidationError: "The manifest shouldn't contain the key field"), # 而 Chrome 侧载固定扩展 ID 依赖 key(见 fluxDown/wxt.config.ts), # 因此上传前解包剔除 key 重新打一个 Edge 专用 zip。API v1.1:ApiKey 鉴权。 # ⚠️ API key 每 72 天过期,过期后需在 Partner Center → Publish API 重新生成并更新 EDGE_API_KEY secret。 - name: Publish Edge extension to Edge Add-ons continue-on-error: true env: EDGE_CLIENT_ID: ${{ secrets.EDGE_CLIENT_ID }} EDGE_API_KEY: ${{ secrets.EDGE_API_KEY }} EDGE_PRODUCT_ID: ${{ secrets.EDGE_PRODUCT_ID }} VERSION: ${{ steps.version.outputs.VERSION }} run: | set -uo pipefail CHROME_ZIP="build/extension/FluxDown-${VERSION}-chrome.zip" BASE="https://api.addons.microsoftedge.microsoft.com/v1/products/${EDGE_PRODUCT_ID}" AUTH=(-H "Authorization: ApiKey ${EDGE_API_KEY}" -H "X-ClientID: ${EDGE_CLIENT_ID}") # 0) 重打 Edge 专用包:剔除 manifest.json 中的 key 字段 PKG_DIR=$(mktemp -d) unzip -q "$CHROME_ZIP" -d "$PKG_DIR" MANIFEST=$(find "$PKG_DIR" -maxdepth 2 -name manifest.json | head -1) if [ -z "$MANIFEST" ]; then echo "::error::manifest.json not found in ${CHROME_ZIP}"; exit 1 fi python3 -c "import json,sys;p=sys.argv[1];m=json.load(open(p));m.pop('key',None);open(p,'w').write(json.dumps(m,indent=2))" "$MANIFEST" ZIP_FILE="$PWD/build/extension/FluxDown-${VERSION}-edge.zip" (cd "$PKG_DIR" && zip -qr "$ZIP_FILE" .) echo "Edge package rebuilt without manifest key: $ZIP_FILE" # 1) 上传 zip 包到 draft(202 + Location 头携带 operationID) echo "Uploading ${ZIP_FILE} to Edge Add-ons..." UP_HEADERS=$(curl -sS -D - -o /dev/null -X POST "${BASE}/submissions/draft/package" \ "${AUTH[@]}" -H "Content-Type: application/zip" --data-binary "@${ZIP_FILE}") OP=$(echo "$UP_HEADERS" | tr -d '\r' | awk -F': ' 'tolower($1)=="location"{print $2}' | awk -F/ '{print $NF}') if [ -z "$OP" ]; then echo "::error::Edge upload did not return an operation id"; echo "$UP_HEADERS"; exit 1 fi echo "Upload operation: $OP" # 2) 轮询上传处理状态(InProgress → Succeeded / Failed) STATUS="?" for i in $(seq 1 20); do sleep 10 RESP=$(curl -sS "${BASE}/submissions/draft/package/operations/${OP}" "${AUTH[@]}") STATUS=$(echo "$RESP" | python3 -c "import sys,json;print(json.load(sys.stdin).get('status','?'))" 2>/dev/null || echo "?") echo " upload status: $STATUS" [ "$STATUS" = "Succeeded" ] && break if [ "$STATUS" = "Failed" ]; then echo "::error::Edge upload failed: $RESP"; exit 1; fi done if [ "$STATUS" != "Succeeded" ]; then echo "::error::Edge upload timed out (status=$STATUS)"; exit 1 fi # 3) 提交发布(202 + Location 头携带发布 operationID) echo "Publishing Edge submission..." PUB_HEADERS=$(curl -sS -D - -o /dev/null -X POST "${BASE}/submissions" \ "${AUTH[@]}" -H "Content-Type: application/json" \ --data "{\"notes\":\"Automated release v${VERSION}\"}") PUB_OP=$(echo "$PUB_HEADERS" | tr -d '\r' | awk -F': ' 'tolower($1)=="location"{print $2}' | awk -F/ '{print $NF}') echo "Publish operation: ${PUB_OP:-}" echo "::notice::Edge extension v${VERSION} submitted to Edge Add-ons review" - name: Upload Firefox extension artifact uses: actions/upload-artifact@v4 with: name: browser-extension-firefox path: build/extension/FluxDown-*-firefox.xpi if-no-files-found: warn # ── 并行 Job 3: 构建 Linux AppImage + tar.gz(x64)── build-linux: name: Build Linux (x64) runs-on: ubuntu-22.04 needs: changes if: needs.changes.outputs.app == 'true' steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Get version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" # ── 系统依赖 ── # 注:已切换到 rustls,无需 libssl-dev(运行时亦无 libssl 依赖) - name: Install system dependencies run: | sudo apt-get update -y sudo apt-get install -y \ cmake ninja-build clang pkg-config \ libgtk-3-dev \ libnotify-dev \ libsecret-1-dev \ patchelf \ libfuse2 \ zstd # ── Rust toolchain ── - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable # 智能 Rust 缓存:自动管理 ~/.cargo 和 workspace target/ # Workspace Cargo.toml 在根目录,target/ 为 hub + nmh 共享目录 - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 with: cache-on-failure: true - name: Cache Rinf CLI id: cache-rinf uses: actions/cache@v4 with: path: ~/.cargo/bin/rinf key: rinf-cli-${{ runner.os }}-x64-8.9.0 # ── Flutter SDK ── - name: Setup Flutter uses: subosito/flutter-action@v2 with: channel: stable cache: true - name: Flutter precache & version info run: | flutter --version flutter precache --linux - name: Install Flutter dependencies run: flutter pub get # ── Rinf code generation ── - name: Install Rinf CLI if: steps.cache-rinf.outputs.cache-hit != 'true' shell: bash env: CARGO_HTTP_MULTIPLEXING: "false" CARGO_NET_RETRY: "10" run: | for i in 1 2 3; do cargo install rinf_cli@8.9.0 && exit 0 echo "cargo install failed (attempt $i), retrying in 15s..." sleep 15 done exit 1 - name: Generate Rinf bindings run: rinf gen # ── Build ── - name: Build Flutter Linux app run: flutter build linux --release --build-name=${{ steps.version.outputs.VERSION }} --build-number=1 --dart-define=APP_VERSION=${{ steps.version.outputs.VERSION }} # NOTE: fluxdown_nmh and fluxdown_updater are built automatically # by CMake custom targets defined in linux/CMakeLists.txt during flutter build. # ── Package tar.gz(命名目录后压缩,解压得 FluxDown-x.y.z-linux-x64/)── - name: Create portable tar.gz run: | VERSION=${{ steps.version.outputs.VERSION }} BUNDLE_DIR="build/linux/x64/release/bundle" DIST_DIR="FluxDown-${VERSION}-linux-x64" mkdir -p build/installer mkdir -p "$DIST_DIR" cp -a "$BUNDLE_DIR/." "$DIST_DIR/" tar -czf "build/installer/FluxDown-${VERSION}-linux-x64.tar.gz" "$DIST_DIR" rm -rf "$DIST_DIR" # ── Package AppImage ── - name: Download appimagetool run: | wget -qO appimagetool-x86_64.AppImage \ "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" chmod +x appimagetool-x86_64.AppImage - name: Build AppImage run: | VERSION=${{ steps.version.outputs.VERSION }} BUNDLE_DIR="build/linux/x64/release/bundle" # 创建 AppDir,顶层即 bundle 内容(flux_down、data/、lib/ 都在根) mkdir -p AppDir cp -a "$BUNDLE_DIR/." AppDir/ # .desktop 文件复制到 AppDir 根(AppImage 约定) cp linux/com.fluxdown.app.desktop AppDir/ # 图标复制到 AppDir 根,文件名必须与 .desktop 中 Icon= 一致 cp assets/logo/fluxdown_logo.png AppDir/com.fluxdown.app.png # AppRun 入口:设置 LD_LIBRARY_PATH,让 flux_down 找到 lib/ 下的共享库 { echo '#!/bin/bash' echo 'HERE="$(dirname "$(readlink -f "${0}")")"' echo 'export LD_LIBRARY_PATH="${HERE}/lib:${LD_LIBRARY_PATH:-}"' echo 'exec "${HERE}/flux_down" "$@"' } > AppDir/AppRun chmod +x AppDir/AppRun # 打包 AppImage(APPIMAGE_EXTRACT_AND_RUN=1 避免 CI 无 FUSE 时失败) ARCH=x86_64 APPIMAGE_EXTRACT_AND_RUN=1 \ ./appimagetool-x86_64.AppImage AppDir \ "build/installer/FluxDown-${VERSION}-linux-x64.AppImage" # ── Package deb(Debian/Ubuntu)── # 安装布局:Flutter bundle → /opt/fluxdown/,wrapper 脚本 → /usr/bin/flux_down # 保持 RPATH=$ORIGIN/lib 正确(可执行文件在 /opt/fluxdown/,lib 在 /opt/fluxdown/lib/) # Flutter 查找 data/ 目录时用 argv[0]=/opt/fluxdown/flux_down,找 /opt/fluxdown/data/ ✓ - name: Create deb package run: | VERSION=${{ steps.version.outputs.VERSION }} BUNDLE_DIR="build/linux/x64/release/bundle" PKG_DIR="deb_pkg" # 创建目录结构 mkdir -p "$PKG_DIR/DEBIAN" mkdir -p "$PKG_DIR/opt/fluxdown" mkdir -p "$PKG_DIR/usr/bin" mkdir -p "$PKG_DIR/usr/share/applications" mkdir -p "$PKG_DIR/usr/share/icons/hicolor/256x256/apps" # 复制 Flutter bundle 到 /opt/fluxdown/(保持原有相对目录) cp -a "$BUNDLE_DIR/." "$PKG_DIR/opt/fluxdown/" # /usr/bin/flux_down 是 wrapper 脚本,将启动 /opt/fluxdown/flux_down { echo '#!/bin/bash' echo 'exec /opt/fluxdown/flux_down "$@"' } > "$PKG_DIR/usr/bin/flux_down" chmod 755 "$PKG_DIR/usr/bin/flux_down" # 复制 .desktop 文件和图标到标准 XDG 路径 cp linux/com.fluxdown.app.desktop "$PKG_DIR/usr/share/applications/" cp assets/logo/fluxdown_logo.png \ "$PKG_DIR/usr/share/icons/hicolor/256x256/apps/com.fluxdown.app.png" # 计算安装后总大小(KB,供 control 字段使用) INSTALLED_SIZE_KB=$(du -sk "$PKG_DIR/opt/" "$PKG_DIR/usr/" \ | awk '{sum+=$1} END {print sum}') # 生成 DEBIAN/control { echo "Package: fluxdown" echo "Version: ${VERSION}" echo "Architecture: amd64" echo "Maintainer: FluxDown Team " echo "Homepage: https://fluxdown.zerx.dev" echo "Section: net" echo "Priority: optional" echo "Installed-Size: ${INSTALLED_SIZE_KB}" echo "Depends: libgtk-3-0, libnotify4" echo "Description: Free IDM-alternative download manager" echo " FluxDown is a Rust-powered, open-source download manager." echo " Supports HTTP/HTTPS/FTP, BitTorrent magnetic links, and HLS/DASH streaming" echo " with multi-threaded acceleration and seamless browser integration." } > "$PKG_DIR/DEBIAN/control" # 生成 DEBIAN/postinst(安装后刷新桌面数据库和图标缓存) { echo '#!/bin/bash' echo 'set -e' echo 'update-desktop-database -q /usr/share/applications/ 2>/dev/null || true' echo 'gtk-update-icon-cache -q /usr/share/icons/hicolor/ 2>/dev/null || true' } > "$PKG_DIR/DEBIAN/postinst" chmod 755 "$PKG_DIR/DEBIAN/postinst" # 打包 deb(--root-owner-group 将所有文件所有者设为 root:root) # 注:NMH JSON 清单由 app 首次运行时自动写入用户目录,卸载时无法从 root 脚本定位, # 残留的 JSON 文件仅指向不存在的二进制,浏览器会静默忽略,无需主动清理。 mkdir -p build/installer dpkg-deb --build --root-owner-group "$PKG_DIR" \ "build/installer/FluxDown-${VERSION}-linux-x64.deb" # ── Package Arch(.pkg.tar.zst)── # 与 deb 相同的安装布局:/opt/fluxdown/ + /usr/bin/ wrapper - name: Create Arch package run: | VERSION=${{ steps.version.outputs.VERSION }} BUNDLE_DIR="build/linux/x64/release/bundle" PKG_DIR="arch_pkg" # 创建目录结构 mkdir -p "$PKG_DIR/opt/fluxdown" mkdir -p "$PKG_DIR/usr/bin" mkdir -p "$PKG_DIR/usr/share/applications" mkdir -p "$PKG_DIR/usr/share/icons/hicolor/256x256/apps" # 复制 Flutter bundle 到 /opt/fluxdown/ cp -a "$BUNDLE_DIR/." "$PKG_DIR/opt/fluxdown/" # /usr/bin/flux_down wrapper 脚本 { echo '#!/bin/bash' echo 'exec /opt/fluxdown/flux_down "$@"' } > "$PKG_DIR/usr/bin/flux_down" chmod 755 "$PKG_DIR/usr/bin/flux_down" # .desktop 和图标 cp linux/com.fluxdown.app.desktop "$PKG_DIR/usr/share/applications/" cp assets/logo/fluxdown_logo.png \ "$PKG_DIR/usr/share/icons/hicolor/256x256/apps/com.fluxdown.app.png" # 计算安装后总字节数(.PKGINFO 的 size 字段) INSTALLED_SIZE=$(du -sb "$PKG_DIR/opt/" "$PKG_DIR/usr/" \ | awk '{sum+=$1} END {print sum}') # 生成 .PKGINFO(pacman 安装包元数据) { echo "# Generated by FluxDown CI" echo "pkgname = fluxdown" echo "pkgver = ${VERSION}-1" echo "pkgdesc = Free IDM-alternative download manager" echo "url = https://fluxdown.zerx.dev" echo "builddate = $(date +%s)" echo "packager = FluxDown CI " echo "size = ${INSTALLED_SIZE}" echo "arch = x86_64" echo "license = custom" echo "depend = gtk3" echo "depend = libnotify" } > "$PKG_DIR/.PKGINFO" # 打包 .pkg.tar.zst(.PKGINFO 必须显式列在首位) # --owner=0 --group=0:确保包内所有文件归属 root:root,避免 CI runner uid 写入包中 mkdir -p build/installer cd "$PKG_DIR" tar --zstd --owner=0 --group=0 -cf \ "../build/installer/FluxDown-${VERSION}-linux-x64.pkg.tar.zst" \ .PKGINFO opt/ usr/ cd .. # ── Upload artifacts(供 release job 使用)── - name: Upload Linux AppImage artifact uses: actions/upload-artifact@v4 with: name: linux-appimage path: build/installer/FluxDown-*-linux-x64.AppImage if-no-files-found: error - name: Upload Linux deb artifact uses: actions/upload-artifact@v4 with: name: linux-deb path: build/installer/FluxDown-*-linux-x64.deb if-no-files-found: error - name: Upload Linux Arch package artifact uses: actions/upload-artifact@v4 with: name: linux-arch path: build/installer/FluxDown-*-linux-x64.pkg.tar.zst if-no-files-found: error - name: Upload Linux portable artifact uses: actions/upload-artifact@v4 with: name: linux-portable path: build/installer/FluxDown-*-linux-x64.tar.gz if-no-files-found: error # ── 并行 Job 4: 构建 macOS DMG + tar.gz(Intel x64 + Apple Silicon arm64)── build-macos: name: Build macOS (${{ matrix.arch }}) needs: changes if: needs.changes.outputs.app == 'true' strategy: fail-fast: false matrix: include: - arch: x64 runner: macos-15-intel # Intel runner (macos-13 已废弃) flutter_target: macos rust_target: x86_64-apple-darwin - arch: arm64 runner: macos-15 # Apple Silicon runner flutter_target: macos rust_target: aarch64-apple-darwin runs-on: ${{ matrix.runner }} steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Get version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" # ── Rust toolchain ── - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.rust_target }} - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 with: cache-on-failure: true - name: Cache Rinf CLI id: cache-rinf uses: actions/cache@v4 with: path: ~/.cargo/bin/rinf key: rinf-cli-${{ runner.os }}-${{ matrix.arch }}-8.9.0 # ── Flutter SDK ── - name: Setup Flutter uses: subosito/flutter-action@v2 with: channel: stable cache: true - name: Flutter precache & version info run: | flutter --version flutter precache --macos - name: Install Flutter dependencies run: flutter pub get # ── Rinf code generation ── - name: Install Rinf CLI if: steps.cache-rinf.outputs.cache-hit != 'true' shell: bash env: CARGO_HTTP_MULTIPLEXING: "false" CARGO_NET_RETRY: "10" run: | for i in 1 2 3; do cargo install rinf_cli@8.9.0 && exit 0 echo "cargo install failed (attempt $i), retrying in 15s..." sleep 15 done exit 1 - name: Generate Rinf bindings run: rinf gen # ── Build ── - name: Build Flutter macOS app run: | flutter build macos --release \ --build-name=${{ steps.version.outputs.VERSION }} \ --build-number=1 \ --dart-define=APP_VERSION=${{ steps.version.outputs.VERSION }} # NOTE: fluxdown_nmh and fluxdown_updater are built automatically # by Xcode shell script build phases defined in macos/Runner.xcodeproj during flutter build. # ── Package tar.gz portable ── - name: Create portable tar.gz run: | VERSION=${{ steps.version.outputs.VERSION }} APP_PATH="build/macos/Build/Products/Release/FluxDown.app" mkdir -p build/installer DIST_DIR="FluxDown-${VERSION}-macos-${{ matrix.arch }}" mkdir -p "$DIST_DIR" cp -R "$APP_PATH" "$DIST_DIR/" tar -czf "build/installer/FluxDown-${VERSION}-macos-${{ matrix.arch }}.tar.gz" "$DIST_DIR" rm -rf "$DIST_DIR" # ── Package DMG ── - name: Install create-dmg run: brew install create-dmg - name: Create DMG run: | VERSION=${{ steps.version.outputs.VERSION }} APP_PATH="build/macos/Build/Products/Release/FluxDown.app" mkdir -p build/installer mkdir -p dmg_src cp -R "$APP_PATH" dmg_src/ create-dmg \ --volname "FluxDown ${VERSION}" \ --window-pos 200 120 \ --window-size 660 400 \ --icon-size 100 \ --icon "FluxDown.app" 160 185 \ --hide-extension "FluxDown.app" \ --app-drop-link 500 185 \ --no-internet-enable \ "build/installer/FluxDown-${VERSION}-macos-${{ matrix.arch }}.dmg" \ dmg_src/ || \ hdiutil create \ -volname "FluxDown ${VERSION}" \ -srcfolder dmg_src \ -ov -format UDZO \ "build/installer/FluxDown-${VERSION}-macos-${{ matrix.arch }}.dmg" # ── Upload artifacts(供 release job 使用)── - name: Upload macOS DMG artifact uses: actions/upload-artifact@v4 with: name: macos-dmg-${{ matrix.arch }} path: build/installer/FluxDown-*-macos-${{ matrix.arch }}.dmg if-no-files-found: error - name: Upload macOS portable artifact uses: actions/upload-artifact@v4 with: name: macos-portable-${{ matrix.arch }} path: build/installer/FluxDown-*-macos-${{ matrix.arch }}.tar.gz if-no-files-found: error # ── 并行 Job 4b: 构建 Android APK(覆盖常用硬件 ABI + 通用包)── # 触发条件:下载引擎(native/engine)或移动端代码(android/**、lib/src/mobile/**、 # 共享 Dart 代码)变更。Rinf 经 cargokit 在 flutter build apk 期间为各 ABI 编译 Rust cdylib。 build-android: name: Build Android APK runs-on: ubuntu-latest needs: changes if: needs.changes.outputs.mobile == 'true' steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Get version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" # ── JDK 17(Android Gradle Plugin 要求)── - name: Setup JDK 17 uses: actions/setup-java@v4 with: distribution: temurin java-version: "17" # ── Android SDK / NDK(Flutter 构建时需要)── - name: Setup Android SDK uses: android-actions/setup-android@v3 # ── Rust toolchain + Android 目标三元组(cargokit 编译各 ABI 的 cdylib)── - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable with: targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 with: cache-on-failure: true - name: Cache Rinf CLI id: cache-rinf uses: actions/cache@v4 with: path: ~/.cargo/bin/rinf key: rinf-cli-${{ runner.os }}-x64-8.9.0 # ── Flutter SDK ── - name: Setup Flutter uses: subosito/flutter-action@v2 with: channel: stable cache: true - name: Flutter version info run: flutter --version - name: Install Flutter dependencies run: flutter pub get # ── Rinf code generation ── - name: Install Rinf CLI if: steps.cache-rinf.outputs.cache-hit != 'true' shell: bash env: CARGO_HTTP_MULTIPLEXING: "false" CARGO_NET_RETRY: "10" run: | for i in 1 2 3; do cargo install rinf_cli@8.9.0 && exit 0 echo "cargo install failed (attempt $i), retrying in 15s..." sleep 15 done exit 1 - name: Generate Rinf bindings run: rinf gen # ── Release 签名密钥(可选)── # 提供 ANDROID_KEYSTORE_BASE64 等 secrets 时生成正式签名 key.properties, # 否则 android/app/build.gradle.kts 自动回退 debug 签名,构建不中断。 - name: Configure release signing shell: bash env: ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} run: | # secrets 不能用于 step 级 if,改为运行时判空:无密钥则跳过,gradle 回退 debug 签名。 if [ -z "$ANDROID_KEYSTORE_BASE64" ]; then echo "::warning::ANDROID_KEYSTORE_BASE64 未配置 — 使用 debug 签名构建 APK(仅供测试,不可上架)" exit 0 fi echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/app/upload-keystore.jks cat > android/key.properties <> "$GITHUB_OUTPUT" - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} # musl-gcc:ring/bundled-sqlite/zstd 等 C 依赖静态编译所需 - name: Install musl toolchain (Linux) if: runner.os == 'Linux' run: sudo apt-get update && sudo apt-get install -y musl-tools - name: Cache Rust dependencies uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target key: server-${{ matrix.platform }}-${{ hashFiles('**/Cargo.lock') }} restore-keys: | server-${{ matrix.platform }}- - name: Build server binary run: cargo build --release --locked -p fluxdown_server --target ${{ matrix.target }} - name: Download webroot artifact uses: actions/download-artifact@v4 with: name: server-webroot path: webroot - name: Package (tar.gz) if: runner.os != 'Windows' shell: bash run: | VERSION=${{ steps.version.outputs.VERSION }} PKG="fluxdown-server-${VERSION}-${{ matrix.platform }}" mkdir -p "build/server/${PKG}" release-assets cp "target/${{ matrix.target }}/release/fluxdown-server" "build/server/${PKG}/" cp -r webroot "build/server/${PKG}/web" tar -C build/server -czf "release-assets/FluxDown-Server-${VERSION}-${{ matrix.platform }}.tar.gz" "${PKG}" ls -la release-assets/ - name: Package (zip) if: runner.os == 'Windows' shell: pwsh run: | $Version = "${{ steps.version.outputs.VERSION }}" $Pkg = "fluxdown-server-$Version-${{ matrix.platform }}" New-Item -ItemType Directory -Force -Path "build\server\$Pkg", "release-assets" | Out-Null Copy-Item "target\${{ matrix.target }}\release\fluxdown-server.exe" "build\server\$Pkg\" Copy-Item -Recurse "webroot" "build\server\$Pkg\web" Compress-Archive -Path "build\server\$Pkg" -DestinationPath "release-assets\FluxDown-Server-$Version-${{ matrix.platform }}.zip" Get-ChildItem release-assets - name: Upload server binary artifact uses: actions/upload-artifact@v4 with: name: server-bin-${{ matrix.platform }} path: release-assets/* # ── 并行 Job: 构建 CLI 二进制(fluxdown,6 平台)── # 与 build-server-binaries 同套 runner/target 策略(全原生 runner 规避 C 依赖交叉编译): # linux → musl 静态链接(零 glibc 依赖) # windows → MSVC + Schannel(native-tls) # macos → 各架构原生 runner 编译 # CLI 是单二进制(无 webroot),产物即 fluxdown(.exe)。 build-cli-binaries: name: Build CLI (${{ matrix.platform }}) needs: changes if: needs.changes.outputs.cli == 'true' strategy: fail-fast: false matrix: include: - platform: linux-x64 runner: ubuntu-latest target: x86_64-unknown-linux-musl - platform: linux-arm64 runner: ubuntu-24.04-arm target: aarch64-unknown-linux-musl - platform: windows-x64 runner: windows-latest target: x86_64-pc-windows-msvc - platform: windows-arm64 runner: windows-11-arm target: aarch64-pc-windows-msvc - platform: macos-x64 runner: macos-latest target: x86_64-apple-darwin - platform: macos-arm64 runner: macos-latest target: aarch64-apple-darwin runs-on: ${{ matrix.runner }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Get version from tag id: version shell: bash run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} # musl-gcc:ring 等 C 依赖静态编译所需 - name: Install musl toolchain (Linux) if: runner.os == 'Linux' run: sudo apt-get update && sudo apt-get install -y musl-tools - name: Cache Rust dependencies uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target key: cli-${{ matrix.platform }}-${{ hashFiles('**/Cargo.lock') }} restore-keys: | cli-${{ matrix.platform }}- - name: Build CLI binary run: cargo build --release --locked -p fluxdown_cli --target ${{ matrix.target }} - name: Package (tar.gz) if: runner.os != 'Windows' shell: bash run: | VERSION=${{ steps.version.outputs.VERSION }} PKG="fluxdown-cli-${VERSION}-${{ matrix.platform }}" mkdir -p "build/cli/${PKG}" release-assets cp "target/${{ matrix.target }}/release/fluxdown" "build/cli/${PKG}/" tar -C build/cli -czf "release-assets/FluxDown-CLI-${VERSION}-${{ matrix.platform }}.tar.gz" "${PKG}" ls -la release-assets/ - name: Package (zip) if: runner.os == 'Windows' shell: pwsh run: | $Version = "${{ steps.version.outputs.VERSION }}" $Pkg = "fluxdown-cli-$Version-${{ matrix.platform }}" New-Item -ItemType Directory -Force -Path "build\cli\$Pkg", "release-assets" | Out-Null Copy-Item "target\${{ matrix.target }}\release\fluxdown.exe" "build\cli\$Pkg\" Compress-Archive -Path "build\cli\$Pkg" -DestinationPath "release-assets\FluxDown-CLI-$Version-${{ matrix.platform }}.zip" Get-ChildItem release-assets - name: Upload CLI binary artifact uses: actions/upload-artifact@v4 with: name: cli-bin-${{ matrix.platform }} path: release-assets/* # ── 并行 Job 5c: Docker 镜像(linux/amd64,推 ghcr.io)── # linux 二进制 tarball 已由 build-server-binaries(musl 静态链接)产出,此处只推镜像。 build-server-docker: name: Build Server (Docker) runs-on: ubuntu-latest needs: changes if: needs.changes.outputs.server == 'true' steps: - name: Checkout repository uses: actions/checkout@v4 - name: Get version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # 镜像名要求全小写,metadata-action 自动归一化 - name: Docker image metadata id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository_owner }}/fluxdown-server tags: | type=raw,value=${{ steps.version.outputs.VERSION }} type=raw,value=latest - name: Build and push Docker image uses: docker/build-push-action@v6 with: context: . file: docker/server.Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha,scope=server cache-to: type=gha,scope=server,mode=max # ── Release Job 1: 桌面客户端(使用推送的 v* tag,保持 GitHub latest release 语义)── release-app: name: Release Desktop App runs-on: ubuntu-latest needs: [build-windows, build-linux, build-macos] steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true # 用 git-cliff 从 Conventional Commits 生成本版本的更新内容 # (--latest 仅包含上一个 tag 到当前 tag 之间的 commit, # --strip header 去掉 changelog 文件头,只保留版本区块) - name: Generate release notes with git-cliff uses: orhun/git-cliff-action@v4 with: args: --latest --strip header env: OUTPUT: RELEASE_NOTES.md GITHUB_REPO: ${{ github.repository }} # 兜底:若区间内没有符合规范的 commit(git-cliff 输出为空), # 使用默认标题避免空 release body;同时备份原始 notes 供翻译失败时回退 - name: Finalize release notes id: finalize shell: bash run: | TAG_NAME="${GITHUB_REF#refs/tags/}" HAS_NOTES=true if [ ! -s RELEASE_NOTES.md ]; then echo "Release $TAG_NAME" > RELEASE_NOTES.md HAS_NOTES=false fi cp RELEASE_NOTES.md RELEASE_NOTES.orig.md echo "has_notes=$HAS_NOTES" >> "$GITHUB_OUTPUT" echo "── Release notes (git-cliff) ──" cat RELEASE_NOTES.md # ── 将混合语言的 cliff 输出翻译为中英双语 ── # 官网 changelog 页按站点语言取对应区块( 标记), # GitHub Release 页则两段依次展示。失败不阻塞发布(下一步回退原始 notes)。 # 注意:直接调用 Claude Code CLI 而非 claude-code-action —— 该 action 的事件白名单 # 不含 push,tag 推送触发时会立即报 "Unsupported event type: push"。 - name: Translate release notes (bilingual) if: steps.finalize.outputs.has_notes == 'true' continue-on-error: true shell: bash env: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} TRANSLATE_PROMPT: | RELEASE_NOTES.md 是 git-cliff 从 Conventional Commits 生成的更新日志,条目语言混杂(部分中文、部分英文)。 请将其改写为中英双语版本,并覆盖写回 RELEASE_NOTES.md。 输出文件格式(严格遵守,无代码块围栏、无任何额外说明文字): (完整中文版) (完整英文版) 规则: - 两个区块的结构与原文件一一对应:保留 "## [x.y.z] - 日期" 版本标题、"### 分组" 标题、"- *(scope)* 描述" 条目格式与条目顺序,emoji 原样保留 - 中文区块:分组标题与所有条目描述均为中文(如 "### 🚀 新功能"、"### 🐛 问题修复") - 英文区块:分组标题与所有条目描述均为英文(如 "### 🚀 Features"、"### 🐛 Bug Fixes") - *(scope)* 括号内的 scope 名、反引号内的代码、issue/PR 编号(#123)、专有名词(HLS、DASH、aria2、Rinf、BT tracker 等)原样保留,不翻译 - 只修改 RELEASE_NOTES.md,不改动其他任何文件 run: | npm install -g @anthropic-ai/claude-code claude -p "$TRANSLATE_PROMPT" --allowedTools "Read,Write,Edit" # 校验翻译结果:缺少双语标记则回退到原始 git-cliff 输出 - name: Validate translated notes if: steps.finalize.outputs.has_notes == 'true' shell: bash run: | if grep -q '' RELEASE_NOTES.md \ && grep -q '' RELEASE_NOTES.md; then echo "Bilingual translation OK" else echo "::warning::Translated notes missing language markers — falling back to original git-cliff output" cp RELEASE_NOTES.orig.md RELEASE_NOTES.md fi rm -f RELEASE_NOTES.orig.md echo "── Final release notes ──" cat RELEASE_NOTES.md # ── 下载桌面客户端构建产物 ── - name: Download all Windows artifacts uses: actions/download-artifact@v4 with: pattern: windows-* path: release-assets/ merge-multiple: true - name: Download Linux artifacts uses: actions/download-artifact@v4 with: pattern: linux-* path: release-assets/ merge-multiple: true - name: Download macOS artifacts uses: actions/download-artifact@v4 with: pattern: macos-* path: release-assets/ merge-multiple: true # 生成校验和(供用户校验 + Scoop autoupdate 的 hash.url 引用) - name: Generate SHA256 checksums working-directory: release-assets run: sha256sum * > SHA256SUMS.txt - name: List release assets run: ls -la release-assets/ # ── 创建客户端 Release 并上传所有文件 ── - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: files: release-assets/* body_path: RELEASE_NOTES.md generate_release_notes: false draft: false make_latest: "true" # ── Release Job 2: 浏览器扩展(独立 tag extension-v*,不抢占 latest)── release-extension: name: Release Browser Extension runs-on: ubuntu-latest needs: build-extension steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true - name: Get version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" # 只收集 fluxDown/ 目录相关的 commit 作为扩展 release notes - name: Generate release notes with git-cliff uses: orhun/git-cliff-action@v4 with: args: --latest --strip header --include-path "fluxDown/**" env: OUTPUT: RELEASE_NOTES.md GITHUB_REPO: ${{ github.repository }} # 兜底 + 备份原始 notes 供翻译失败时回退 - name: Finalize release notes id: finalize shell: bash run: | HAS_NOTES=true if [ ! -s RELEASE_NOTES.md ]; then echo "Browser Extension Release v${{ steps.version.outputs.VERSION }}" > RELEASE_NOTES.md HAS_NOTES=false fi cp RELEASE_NOTES.md RELEASE_NOTES.orig.md echo "has_notes=$HAS_NOTES" >> "$GITHUB_OUTPUT" echo "── Release notes (git-cliff) ──" cat RELEASE_NOTES.md # ── 将混合语言的 cliff 输出翻译为中英双语(与 release-app 同一套规则,直接调用 CLI)── - name: Translate release notes (bilingual) if: steps.finalize.outputs.has_notes == 'true' continue-on-error: true shell: bash env: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} TRANSLATE_PROMPT: | RELEASE_NOTES.md 是 git-cliff 从 Conventional Commits 生成的更新日志,条目语言混杂(部分中文、部分英文)。 请将其改写为中英双语版本,并覆盖写回 RELEASE_NOTES.md。 输出文件格式(严格遵守,无代码块围栏、无任何额外说明文字): (完整中文版) (完整英文版) 规则: - 两个区块的结构与原文件一一对应:保留 "## [x.y.z] - 日期" 版本标题、"### 分组" 标题、"- *(scope)* 描述" 条目格式与条目顺序,emoji 原样保留 - 中文区块:分组标题与所有条目描述均为中文(如 "### 🚀 新功能"、"### 🐛 问题修复") - 英文区块:分组标题与所有条目描述均为英文(如 "### 🚀 Features"、"### 🐛 Bug Fixes") - *(scope)* 括号内的 scope 名、反引号内的代码、issue/PR 编号(#123)、专有名词(HLS、DASH、aria2、Content-Disposition 等)原样保留,不翻译 - 只修改 RELEASE_NOTES.md,不改动其他任何文件 run: | npm install -g @anthropic-ai/claude-code claude -p "$TRANSLATE_PROMPT" --allowedTools "Read,Write,Edit" # 校验翻译结果:缺少双语标记则回退到原始 git-cliff 输出 - name: Validate translated notes if: steps.finalize.outputs.has_notes == 'true' shell: bash run: | if grep -q '' RELEASE_NOTES.md \ && grep -q '' RELEASE_NOTES.md; then echo "Bilingual translation OK" else echo "::warning::Translated notes missing language markers — falling back to original git-cliff output" cp RELEASE_NOTES.orig.md RELEASE_NOTES.md fi rm -f RELEASE_NOTES.orig.md echo "── Final release notes ──" cat RELEASE_NOTES.md - name: Download browser extensions uses: actions/download-artifact@v4 with: pattern: browser-extension-* path: release-assets/ merge-multiple: true - name: List release assets run: ls -la release-assets/ # make_latest=false:避免组件 release 成为 GitHub /releases/latest, # 否则会打破客户端自动更新和官网下载页的最新版本判断 - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: extension-v${{ steps.version.outputs.VERSION }} name: FluxDown Extension v${{ steps.version.outputs.VERSION }} files: release-assets/* body_path: RELEASE_NOTES.md generate_release_notes: false draft: false make_latest: "false" # ── Release Job 3: Headless 服务器(独立 tag server-v*,不抢占 latest)── release-server: name: Release Server runs-on: ubuntu-latest needs: [build-server-docker, build-server-binaries] steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true - name: Get version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" # 只收集服务器相关目录的 commit 作为 release notes - name: Generate release notes with git-cliff uses: orhun/git-cliff-action@v4 with: args: --latest --strip header --include-path "native/server/**" --include-path "web/**" --include-path "native/engine/**" --include-path "docker/**" env: OUTPUT: RELEASE_NOTES.md GITHUB_REPO: ${{ github.repository }} # 兜底 + 备份原始 notes 供翻译失败时回退 - name: Finalize release notes id: finalize shell: bash run: | HAS_NOTES=true if [ ! -s RELEASE_NOTES.md ]; then echo "FluxDown Server Release v${{ steps.version.outputs.VERSION }}" > RELEASE_NOTES.md HAS_NOTES=false fi cp RELEASE_NOTES.md RELEASE_NOTES.orig.md echo "has_notes=$HAS_NOTES" >> "$GITHUB_OUTPUT" echo "── Release notes (git-cliff) ──" cat RELEASE_NOTES.md # ── 将混合语言的 cliff 输出翻译为中英双语(与 release-app 同一套规则,直接调用 CLI)── - name: Translate release notes (bilingual) if: steps.finalize.outputs.has_notes == 'true' continue-on-error: true shell: bash env: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} TRANSLATE_PROMPT: | RELEASE_NOTES.md 是 git-cliff 从 Conventional Commits 生成的更新日志,条目语言混杂(部分中文、部分英文)。 请将其改写为中英双语版本,并覆盖写回 RELEASE_NOTES.md。 输出文件格式(严格遵守,无代码块围栏、无任何额外说明文字): (完整中文版) (完整英文版) 规则: - 两个区块的结构与原文件一一对应:保留 "## [x.y.z] - 日期" 版本标题、"### 分组" 标题、"- *(scope)* 描述" 条目格式与条目顺序,emoji 原样保留 - 中文区块:分组标题与所有条目描述均为中文(如 "### 🚀 新功能"、"### 🐛 问题修复") - 英文区块:分组标题与所有条目描述均为英文(如 "### 🚀 Features"、"### 🐛 Bug Fixes") - *(scope)* 括号内的 scope 名、反引号内的代码、issue/PR 编号(#123)、专有名词(HLS、DASH、aria2、Docker、WebSocket 等)原样保留,不翻译 - 只修改 RELEASE_NOTES.md,不改动其他任何文件 run: | npm install -g @anthropic-ai/claude-code claude -p "$TRANSLATE_PROMPT" --allowedTools "Read,Write,Edit" # 校验翻译结果:缺少双语标记则回退到原始 git-cliff 输出 - name: Validate translated notes if: steps.finalize.outputs.has_notes == 'true' shell: bash run: | if grep -q '' RELEASE_NOTES.md \ && grep -q '' RELEASE_NOTES.md; then echo "Bilingual translation OK" else echo "::warning::Translated notes missing language markers — falling back to original git-cliff output" cp RELEASE_NOTES.orig.md RELEASE_NOTES.md fi rm -f RELEASE_NOTES.orig.md echo "── Final release notes ──" cat RELEASE_NOTES.md # 附加下载方式说明(直接下载二进制 + Docker) - name: Append download & Docker usage to release notes shell: bash run: | VERSION=${{ steps.version.outputs.VERSION }} cat >> RELEASE_NOTES.md < SHA256SUMS.txt - name: List release assets run: ls -la release-assets/ # make_latest=false:避免组件 release 成为 GitHub /releases/latest, # 否则会打破客户端自动更新和官网下载页的最新版本判断 - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: server-v${{ steps.version.outputs.VERSION }} name: FluxDown Server v${{ steps.version.outputs.VERSION }} files: release-assets/* body_path: RELEASE_NOTES.md generate_release_notes: false draft: false make_latest: "false" # ── Release Job: CLI(独立 tag cli-v*,不抢占 latest)── release-cli: name: Release CLI runs-on: ubuntu-latest needs: build-cli-binaries steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true - name: Get version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" # 只收集 CLI 相关目录的 commit 作为 release notes - name: Generate release notes with git-cliff uses: orhun/git-cliff-action@v4 with: args: --latest --strip header --include-path "native/cli/**" --include-path "native/api/**" --include-path "native/engine/**" env: OUTPUT: RELEASE_NOTES.md GITHUB_REPO: ${{ github.repository }} # 兜底 + 备份原始 notes 供翻译失败时回退 - name: Finalize release notes id: finalize shell: bash run: | HAS_NOTES=true if [ ! -s RELEASE_NOTES.md ]; then echo "FluxDown CLI Release v${{ steps.version.outputs.VERSION }}" > RELEASE_NOTES.md HAS_NOTES=false fi cp RELEASE_NOTES.md RELEASE_NOTES.orig.md echo "has_notes=$HAS_NOTES" >> "$GITHUB_OUTPUT" echo "── Release notes (git-cliff) ──" cat RELEASE_NOTES.md # ── 将混合语言的 cliff 输出翻译为中英双语(与 release-app 同一套规则,直接调用 CLI)── - name: Translate release notes (bilingual) if: steps.finalize.outputs.has_notes == 'true' continue-on-error: true shell: bash env: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} TRANSLATE_PROMPT: | RELEASE_NOTES.md 是 git-cliff 从 Conventional Commits 生成的更新日志,条目语言混杂(部分中文、部分英文)。 请将其改写为中英双语版本,并覆盖写回 RELEASE_NOTES.md。 输出文件格式(严格遵守,无代码块围栏、无任何额外说明文字): (完整中文版) (完整英文版) 规则: - 两个区块的结构与原文件一一对应:保留 "## [x.y.z] - 日期" 版本标题、"### 分组" 标题、"- *(scope)* 描述" 条目格式与条目顺序,emoji 原样保留 - 中文区块:分组标题与所有条目描述均为中文(如 "### 🚀 新功能"、"### 🐛 问题修复") - 英文区块:分组标题与所有条目描述均为英文(如 "### 🚀 Features"、"### 🐛 Bug Fixes") - *(scope)* 括号内的 scope 名、反引号内的代码、issue/PR 编号(#123)、专有名词(HLS、DASH、aria2、Docker、WebSocket 等)原样保留,不翻译 - 只修改 RELEASE_NOTES.md,不改动其他任何文件 run: | npm install -g @anthropic-ai/claude-code claude -p "$TRANSLATE_PROMPT" --allowedTools "Read,Write,Edit" # 校验翻译结果:缺少双语标记则回退到原始 git-cliff 输出 - name: Validate translated notes if: steps.finalize.outputs.has_notes == 'true' shell: bash run: | if grep -q '' RELEASE_NOTES.md \ && grep -q '' RELEASE_NOTES.md; then echo "Bilingual translation OK" else echo "::warning::Translated notes missing language markers — falling back to original git-cliff output" cp RELEASE_NOTES.orig.md RELEASE_NOTES.md fi rm -f RELEASE_NOTES.orig.md echo "── Final release notes ──" cat RELEASE_NOTES.md # 附加下载 & 安装说明 - name: Append download & install usage to release notes shell: bash run: | VERSION=${{ steps.version.outputs.VERSION }} cat >> RELEASE_NOTES.md < SHA256SUMS.txt - name: List release assets run: ls -la release-assets/ # make_latest=false:组件 release 不抢占 GitHub /releases/latest - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: cli-v${{ steps.version.outputs.VERSION }} name: FluxDown CLI v${{ steps.version.outputs.VERSION }} files: release-assets/* body_path: RELEASE_NOTES.md generate_release_notes: false draft: false make_latest: "false" # ── Release Job 4: 移动端(独立 tag mobile-v*,不抢占 latest)── release-mobile: name: Release Mobile runs-on: ubuntu-latest needs: build-android steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true - name: Get version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" # 只收集移动端相关目录的 commit 作为 release notes - name: Generate release notes with git-cliff uses: orhun/git-cliff-action@v4 with: args: --latest --strip header --include-path "native/engine/**" --include-path "android/**" --include-path "lib/src/mobile/**" env: OUTPUT: RELEASE_NOTES.md GITHUB_REPO: ${{ github.repository }} # 兜底 + 备份原始 notes 供翻译失败时回退 - name: Finalize release notes id: finalize shell: bash run: | HAS_NOTES=true if [ ! -s RELEASE_NOTES.md ]; then echo "Mobile Release v${{ steps.version.outputs.VERSION }}" > RELEASE_NOTES.md HAS_NOTES=false fi cp RELEASE_NOTES.md RELEASE_NOTES.orig.md echo "has_notes=$HAS_NOTES" >> "$GITHUB_OUTPUT" echo "── Release notes (git-cliff) ──" cat RELEASE_NOTES.md # ── 将混合语言的 cliff 输出翻译为中英双语(与 release-app 同一套规则,直接调用 CLI)── - name: Translate release notes (bilingual) if: steps.finalize.outputs.has_notes == 'true' continue-on-error: true shell: bash env: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} TRANSLATE_PROMPT: | RELEASE_NOTES.md 是 git-cliff 从 Conventional Commits 生成的更新日志,条目语言混杂(部分中文、部分英文)。 请将其改写为中英双语版本,并覆盖写回 RELEASE_NOTES.md。 输出文件格式(严格遵守,无代码块围栏、无任何额外说明文字): (完整中文版) (完整英文版) 规则: - 两个区块的结构与原文件一一对应:保留 "## [x.y.z] - 日期" 版本标题、"### 分组" 标题、"- *(scope)* 描述" 条目格式与条目顺序,emoji 原样保留 - 中文区块:分组标题与所有条目描述均为中文(如 "### 🚀 新功能"、"### 🐛 问题修复") - 英文区块:分组标题与所有条目描述均为英文(如 "### 🚀 Features"、"### 🐛 Bug Fixes") - *(scope)* 括号内的 scope 名、反引号内的代码、issue/PR 编号(#123)、专有名词(HLS、DASH、aria2、Rinf、BT tracker 等)原样保留,不翻译 - 只修改 RELEASE_NOTES.md,不改动其他任何文件 run: | npm install -g @anthropic-ai/claude-code claude -p "$TRANSLATE_PROMPT" --allowedTools "Read,Write,Edit" # 校验翻译结果:缺少双语标记则回退到原始 git-cliff 输出 - name: Validate translated notes if: steps.finalize.outputs.has_notes == 'true' shell: bash run: | if grep -q '' RELEASE_NOTES.md \ && grep -q '' RELEASE_NOTES.md; then echo "Bilingual translation OK" else echo "::warning::Translated notes missing language markers — falling back to original git-cliff output" cp RELEASE_NOTES.orig.md RELEASE_NOTES.md fi rm -f RELEASE_NOTES.orig.md echo "── Final release notes ──" cat RELEASE_NOTES.md - name: Download Android APK artifacts uses: actions/download-artifact@v4 with: pattern: android-apk path: release-assets/ merge-multiple: true - name: Generate SHA256 checksums working-directory: release-assets run: sha256sum * > SHA256SUMS.txt - name: List release assets run: ls -la release-assets/ # make_latest=false:组件 release 不抢占 GitHub /releases/latest - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: mobile-v${{ steps.version.outputs.VERSION }} name: FluxDown Mobile v${{ steps.version.outputs.VERSION }} files: release-assets/* body_path: RELEASE_NOTES.md generate_release_notes: false draft: false make_latest: "false" # ── Release 后置 Job: 自动更新自托管 Scoop bucket manifest(bucket/fluxdown.json)── # 桌面客户端 release 成功后,用本次版本号 + 真实 SHA256 刷新 manifest 并回推默认分支, # 使 `scoop bucket add fluxdown https://github.com/zerx-lab/FluxDown` 的用户始终装到最新版。 # 官方 bucket(ScoopInstaller/Extras)另有 excavator 机器人按 autoupdate 段自动更新,无需此 job。 update-scoop-bucket: name: Update Scoop Bucket runs-on: ubuntu-latest needs: release-app permissions: contents: write steps: # 检出默认分支(非 tag 的 detached HEAD),以便 commit 回推 - name: Checkout default branch uses: actions/checkout@v4 with: ref: ${{ github.event.repository.default_branch }} fetch-depth: 0 - name: Get version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" # 从已发布的 release 资产下载 Windows 便携版,计算真实 SHA256 并写入 manifest - name: Update manifest (version + url + hash) env: VERSION: ${{ steps.version.outputs.VERSION }} run: | set -euo pipefail BASE="https://github.com/${{ github.repository }}/releases/download/v${VERSION}" X64_ZIP="FluxDown-${VERSION}-windows-x64-portable.zip" ARM_ZIP="FluxDown-${VERSION}-windows-arm64-portable.zip" curl -fsSL -o x64.zip "${BASE}/${X64_ZIP}" curl -fsSL -o arm.zip "${BASE}/${ARM_ZIP}" X64_HASH=$(sha256sum x64.zip | awk '{print $1}') ARM_HASH=$(sha256sum arm.zip | awk '{print $1}') rm -f x64.zip arm.zip python3 - </dev/null; then echo " 🗑️ Deleted artifact ${id}" else echo " ⚠️ Failed to delete artifact ${id}" FAIL=$((FAIL+1)) fi done echo "✅ Artifact cleanup complete. Deleted $((DELETE_COUNT-FAIL))/${DELETE_COUNT}" if [ "$FAIL" -gt 0 ]; then echo "⚠️ ${FAIL} deletion(s) failed (will be retried next release)" fi