ci(release): 按组件拆分独立 release 并按 tag 范围检测变更 #1
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| permissions: | |
| contents: write | |
| actions: write # 允许在 release job 末尾删除旧 artifact(避免 500MB 配额耗尽) | |
| jobs: | |
| # ── 前置 Job: 检测上一个版本 tag → 当前 tag 之间哪些组件目录有变更 ── | |
| # 规则: | |
| # fluxDown/** → 浏览器扩展(extension) | |
| # website/** → 官网(website) | |
| # 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 }} | |
| website: ${{ steps.detect.outputs.website }} | |
| 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; WEBSITE=false | |
| if [ -z "$PREV" ]; then | |
| echo "No previous version tag found — building everything" | |
| APP=true; EXTENSION=true; WEBSITE=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 ;; | |
| website/*) WEBSITE=true ;; | |
| docs/*|*.md) ;; | |
| *) APP=true ;; | |
| esac | |
| done <<< "$CHANGED" | |
| fi | |
| { | |
| echo "app=$APP" | |
| echo "extension=$EXTENSION" | |
| echo "website=$WEBSITE" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "::notice::Components changed in ${PREV:-<none>}..${TAG}: app=$APP extension=$EXTENSION website=$WEBSITE" | |
| # ── 并行 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 | |
| } | |
| - name: Download Inno Setup Chinese Simplified language file | |
| shell: pwsh | |
| run: | | |
| $issLangDir = "C:\Program Files (x86)\Inno Setup 6\Languages" | |
| $url = "https://raw.githubusercontent.com/jrsoftware/issrc/main/Files/Languages/Unofficial/ChineseSimplified.isl" | |
| Invoke-WebRequest -Uri $url -OutFile "$issLangDir\ChineseSimplified.isl" | |
| - 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 zip(同为 MV3)。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 | |
| ZIP_FILE="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}") | |
| # 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:-<none>}" | |
| 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 <contact@fluxdown.app>" | |
| echo "Homepage: https://fluxdown.app" | |
| 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.app" | |
| echo "builddate = $(date +%s)" | |
| echo "packager = FluxDown CI <ci@fluxdown.app>" | |
| 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 5: 构建官网(website/ 有变更时)── | |
| build-website: | |
| name: Build Website | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: needs.changes.outputs.website == '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" | |
| # website/ 无 package-lock.json,不能用 npm ci | |
| - name: Install dependencies | |
| run: npm install | |
| working-directory: website | |
| - name: Build website | |
| run: npm run build | |
| working-directory: website | |
| # 打包 Node standalone 构建产物(dist/ 含 server + client) | |
| - name: Package website dist | |
| run: | | |
| VERSION=${{ steps.version.outputs.VERSION }} | |
| mkdir -p build/website | |
| tar -czf "build/website/FluxDown-website-${VERSION}.tar.gz" -C website dist | |
| ls -la build/website/ | |
| - name: Upload website artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: website-dist | |
| path: build/website/FluxDown-website-*.tar.gz | |
| if-no-files-found: error | |
| # ── 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 | |
| - name: Finalize release notes | |
| shell: bash | |
| run: | | |
| TAG_NAME="${GITHUB_REF#refs/tags/}" | |
| if [ ! -s RELEASE_NOTES.md ]; then | |
| echo "Release $TAG_NAME" > RELEASE_NOTES.md | |
| fi | |
| echo "── 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 | |
| - 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 }} | |
| - name: Finalize release notes | |
| shell: bash | |
| run: | | |
| if [ ! -s RELEASE_NOTES.md ]; then | |
| echo "Browser Extension Release v${{ steps.version.outputs.VERSION }}" > RELEASE_NOTES.md | |
| fi | |
| echo "── 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: 官网(独立 tag website-v*,不抢占 latest)── | |
| release-website: | |
| name: Release Website | |
| runs-on: ubuntu-latest | |
| needs: build-website | |
| 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" | |
| # 只收集 website/ 目录相关的 commit 作为官网 release notes | |
| - name: Generate release notes with git-cliff | |
| uses: orhun/git-cliff-action@v4 | |
| with: | |
| args: --latest --strip header --include-path "website/**" | |
| env: | |
| OUTPUT: RELEASE_NOTES.md | |
| GITHUB_REPO: ${{ github.repository }} | |
| - name: Finalize release notes | |
| shell: bash | |
| run: | | |
| if [ ! -s RELEASE_NOTES.md ]; then | |
| echo "Website Release v${{ steps.version.outputs.VERSION }}" > RELEASE_NOTES.md | |
| fi | |
| echo "── Release notes ──" | |
| cat RELEASE_NOTES.md | |
| - name: Download website artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: website-dist | |
| path: release-assets/ | |
| merge-multiple: true | |
| - name: List release assets | |
| run: ls -la release-assets/ | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: website-v${{ steps.version.outputs.VERSION }} | |
| name: FluxDown Website v${{ steps.version.outputs.VERSION }} | |
| files: release-assets/* | |
| body_path: RELEASE_NOTES.md | |
| generate_release_notes: false | |
| draft: false | |
| make_latest: "false" | |
| # ── 最终 Job: 清理 GitHub Actions Artifact,仅保留当前 tag 这一组 ── | |
| # 原因:Free 账号 artifact 配额仅 500MB,每次 release 会上传 ~400MB | |
| # Release 资产已独立存储在 GitHub Release,artifact 只是 CI 中间产物可安全清理 | |
| # always():无论哪些组件 release 被跳过/失败都执行清理,失败不阻断(continue-on-error) | |
| cleanup-artifacts: | |
| name: Cleanup CI Artifacts | |
| runs-on: ubuntu-latest | |
| needs: [release-app, release-extension, release-website] | |
| if: ${{ always() && !cancelled() }} | |
| steps: | |
| - name: Cleanup GitHub Actions artifacts (keep current tag only) | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| CURRENT_TAG="${GITHUB_REF#refs/tags/}" | |
| echo "🧹 Artifact cleanup: keeping only artifacts from tag ${CURRENT_TAG}" | |
| # 列出所有 artifact,过滤掉当前 tag 的,删除其余所有 | |
| # workflow_run.head_branch 在 tag 推送时等于 tag 名(v0.1.43 这样) | |
| DELETE_IDS=$(gh api --paginate \ | |
| repos/${{ github.repository }}/actions/artifacts \ | |
| -q ".artifacts[] | select(.workflow_run.head_branch != \"${CURRENT_TAG}\") | .id") | |
| if [ -z "$DELETE_IDS" ]; then | |
| echo "✅ No old artifacts to delete" | |
| exit 0 | |
| fi | |
| DELETE_COUNT=$(echo "$DELETE_IDS" | wc -l) | |
| echo "Will delete ${DELETE_COUNT} old artifact(s)" | |
| # 串行删除避免触发 GitHub TLS 限流(之前并发 8 大量 timeout) | |
| FAIL=0 | |
| for id in $DELETE_IDS; do | |
| if gh api -X DELETE repos/${{ github.repository }}/actions/artifacts/${id} 2>/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 |