-
Notifications
You must be signed in to change notification settings - Fork 44
2241 lines (1950 loc) · 93.2 KB
/
Copy pathrelease.yml
File metadata and controls
2241 lines (1950 loc) · 93.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
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
# 分支守卫:前沿版 tag(vX.Y.Z-rc.N)必须打在 develop 分支上,
# 稳定版 tag(vX.Y.Z)必须打在 main 分支上。打错分支整条流水线在此失败。
- name: Verify tag branch (stable=main, frontier=develop)
shell: bash
run: |
TAG="${GITHUB_REF#refs/tags/}"
case "$TAG" in
*-*) BRANCH=develop ;;
*) BRANCH=main ;;
esac
git fetch origin "$BRANCH" --quiet
if git merge-base --is-ancestor "$TAG" "origin/$BRANCH"; then
echo "::notice::Tag $TAG is on origin/$BRANCH — OK"
else
echo "::error::Tag $TAG is NOT on origin/$BRANCH(稳定版须从 main 打,前沿版 -rc.N 须从 develop 打)"
exit 1
fi
- 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 ;;
packaging/*) 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:-<none>}..${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
fetch-tags: true
- name: Get version from tag
id: version
shell: bash
run: |
V="${GITHUB_REF#refs/tags/v}"
echo "VERSION=$V" >> "$GITHUB_OUTPUT"
echo "CLEAN_VERSION=${V%%-*}" >> "$GITHUB_OUTPUT"
- name: Compute repo stats (donate card)
id: stats
shell: bash
run: |
echo "FIRST_COMMIT_DATE=$(git log --reverse --format=%ad --date=format:%Y-%m-%d | head -1)" >> "$GITHUB_OUTPUT"
echo "RELEASE_COUNT=$(git tag -l 'v[0-9]*' | wc -l | tr -d ' ')" >> "$GITHUB_OUTPUT"
echo "COMMIT_COUNT=$(git rev-list --count HEAD)" >> "$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.CLEAN_VERSION }} --build-number=1 --dart-define=APP_VERSION=${{ steps.version.outputs.VERSION }} --dart-define=STATS_FIRST_COMMIT_DATE=${{ steps.stats.outputs.FIRST_COMMIT_DATE }} --dart-define=STATS_RELEASE_COUNT=${{ steps.stats.outputs.RELEASE_COUNT }} --dart-define=STATS_COMMIT_COUNT=${{ steps.stats.outputs.COMMIT_COUNT }}
# ── 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
# 浏览器扩展发布后无法更改版本号,故预发布(前沿版)tag 不打包扩展。
if: needs.changes.outputs.extension == 'true' && !contains(github.ref_name, '-')
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:-<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
fetch-tags: true
- name: Get version from tag
id: version
run: |
V="${GITHUB_REF#refs/tags/v}"
echo "VERSION=$V" >> "$GITHUB_OUTPUT"
echo "CLEAN_VERSION=${V%%-*}" >> "$GITHUB_OUTPUT"
- name: Compute repo stats (donate card)
id: stats
run: |
echo "FIRST_COMMIT_DATE=$(git log --reverse --format=%ad --date=format:%Y-%m-%d | head -1)" >> "$GITHUB_OUTPUT"
echo "RELEASE_COUNT=$(git tag -l 'v[0-9]*' | wc -l | tr -d ' ')" >> "$GITHUB_OUTPUT"
echo "COMMIT_COUNT=$(git rev-list --count HEAD)" >> "$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-ubuntu2204-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.CLEAN_VERSION }} --build-number=1 --dart-define=APP_VERSION=${{ steps.version.outputs.VERSION }} --dart-define=STATS_FIRST_COMMIT_DATE=${{ steps.stats.outputs.FIRST_COMMIT_DATE }} --dart-define=STATS_RELEASE_COUNT=${{ steps.stats.outputs.RELEASE_COUNT }} --dart-define=STATS_COMMIT_COUNT=${{ steps.stats.outputs.COMMIT_COUNT }}
# 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.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 <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
fetch-tags: true
- name: Get version from tag
id: version
run: |
V="${GITHUB_REF#refs/tags/v}"
echo "VERSION=$V" >> "$GITHUB_OUTPUT"
echo "CLEAN_VERSION=${V%%-*}" >> "$GITHUB_OUTPUT"
- name: Compute repo stats (donate card)
id: stats
run: |
echo "FIRST_COMMIT_DATE=$(git log --reverse --format=%ad --date=format:%Y-%m-%d | head -1)" >> "$GITHUB_OUTPUT"
echo "RELEASE_COUNT=$(git tag -l 'v[0-9]*' | wc -l | tr -d ' ')" >> "$GITHUB_OUTPUT"
echo "COMMIT_COUNT=$(git rev-list --count HEAD)" >> "$GITHUB_OUTPUT"
# ── Rust toolchain ──
# 预装两个 Apple target:macOS Release 为 universal 构建,Xcode 阶段 hub(Rinf)
# 与 super_native_extensions 两个 cargokit 会并发补装缺失 target,rustup 下载
# 缓存无跨进程锁会偶发竞态(.partial rename 失败);预装后 cargokit 直接跳过。
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin,x86_64-apple-darwin
- 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.CLEAN_VERSION }} \
--build-number=1 \
--dart-define=APP_VERSION=${{ steps.version.outputs.VERSION }} \
--dart-define=STATS_FIRST_COMMIT_DATE=${{ steps.stats.outputs.FIRST_COMMIT_DATE }} \
--dart-define=STATS_RELEASE_COUNT=${{ steps.stats.outputs.RELEASE_COUNT }} \
--dart-define=STATS_COMMIT_COUNT=${{ steps.stats.outputs.COMMIT_COUNT }}
# 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"