-
-
Notifications
You must be signed in to change notification settings - Fork 10
992 lines (910 loc) 路 46.6 KB
/
Copy pathbuild.yml
File metadata and controls
992 lines (910 loc) 路 46.6 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
name: Build AeroFTP
on:
push:
branches: [main]
tags:
- 'v*'
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
# Same idea as checks.yml: a new SHA on a PR or on main should cancel the
# previous 4-leg Tauri matrix, not let it keep burning. Do not cancel a tag
# run: a release build must finish even if something else is dispatched.
concurrency:
group: build-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref_type != 'tag' }}
jobs:
# C2: Linux lint+test used to sit on the packaging critical path
# (clippy ~8m + cargo test ~42m + bundle ~61m serial). This job runs
# them on their own ubuntu-22.04 runner so the 4-leg matrix can bundle
# in parallel. Coverage unchanged; wall clock ~max(50, 61).
linux-validate:
runs-on: ubuntu-22.04
timeout-minutes: 90
steps:
- name: Free disk space (Linux)
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/share/swift
sudo apt-get clean
df -h /
- name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Setup Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: '20'
cache: 'npm'
- name: Install Rust
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: stable
components: clippy
- name: Rust cache
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
with:
workspaces: './src-tauri -> target'
# Same prefix as packaging so a warm main cache is a valid restore
# key shape, but no shared-key: this job id gets its own cache
# after main is warm. That is intended (C1 + C2).
prefix-key: 'v2-whisper-vs18'
# PRs restore; only main saves (Actions cache quota is 10 GB).
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install Linux dependencies
run: |
# Runner-image extras: Microsoft's apt repos 403 on their InRelease often
# enough to redden a run, and nothing here installs from them.
sudo rm -f /etc/apt/sources.list.d/microsoft* /etc/apt/sources.list.d/azure-cli*
sudo apt-get update
# `dbus` is here for `cargo test` / portal_chooser (#510), not for
# packaging. `portal_chooser`'s three cases stand up a throwaway
# session bus with `dbus-daemon` to prove that a portal-less host
# reads as portal-less. Without the daemon those tests skipped, and
# a skip is invisible in the log because a passing test's stderr is
# captured: the pin would have reported success while proving
# nothing. Bundle tools (patchelf, rpm, squashfs-tools, libfuse3-dev)
# stay on the packaging Linux leg.
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev dbus libacl1-dev libacl1
- name: Install npm dependencies
run: npm ci
# R10 (APPENDIX-CLI-DISPATCH): the three sources of truth for the
# AeroFTP version must stay in lockstep. If they drift, an update
# ships a CLI that claims one version while the GUI manifest claims
# another, the AUR pkgver bump targets the wrong tag, and the
# in-app update check compares against the wrong baseline. Cheap
# to run (no build, just text parsing). Moved off the packaging
# Linux leg so it no longer blocks the Tauri bundle.
- name: R10 manifest version match (Cargo.toml, package.json, tauri.conf.json, snapcraft.yaml, and the tag on tag builds)
shell: bash
run: |
CARGO=$(grep -m1 '^version' src-tauri/Cargo.toml | sed 's/.*"\(.*\)".*/\1/')
PKG=$(node -p "require('./package.json').version")
TAURI=$(node -p "require('./src-tauri/tauri.conf.json').version")
SNAP=$(grep -m1 '^version:' snap/snapcraft.yaml | sed "s/.*['\"]\(.*\)['\"].*/\1/")
echo "Cargo.toml: $CARGO"
echo "package.json: $PKG"
echo "tauri.conf.json: $TAURI"
echo "snapcraft.yaml: $SNAP"
if [ "$CARGO" != "$PKG" ] || [ "$CARGO" != "$TAURI" ] || [ "$CARGO" != "$SNAP" ]; then
echo "::error::R10 version drift: cargo=$CARGO package.json=$PKG tauri.conf.json=$TAURI snapcraft.yaml=$SNAP"
exit 1
fi
# On a tag build the four manifests must also equal the tag itself. R10
# previously compared them only to each other, so a release tagged vX.Y.Z
# with every manifest left at the prior version passed in silence and
# shipped artifacts labelled with the wrong version. The manifests are
# already proven equal above, so comparing one of them to the tag suffices.
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
TAG="${GITHUB_REF_NAME#v}"
echo "tag: $TAG"
if [ "$CARGO" != "$TAG" ]; then
echo "::error::R10 tag drift: manifests=$CARGO but tag=$TAG"
exit 1
fi
fi
- name: TypeScript type check
run: npx tsc --noEmit
- name: i18n validation
run: npm run i18n:validate
- name: Rust linting (clippy)
working-directory: src-tauri
run: cargo clippy --all-targets -- -D warnings
- name: Rust tests
working-directory: src-tauri
run: cargo test
build-and-release:
# Required for creating releases and Sigstore keyless signing via GitHub OIDC.
permissions:
contents: write
id-token: write
strategy:
fail-fast: false
matrix:
include:
- platform: ubuntu-22.04
target: linux
- platform: windows-latest
target: windows
# Two per-arch macOS DMGs, both built on macos-14 (Sonoma). Pinned
# to macos-14 (not macos-latest) because Tauri's `bundle_dmg.sh`
# fails on macOS 15 (Sequoia). The Intel leg cross-compiles to
# x86_64-apple-darwin on the arm64 runner: a single `universal`
# leg is intentionally avoided because tauri's universal pseudo-
# target lipo's only the main GUI binary, leaving the extra package
# bins (aeroftp-cli, aeroftp-dispatch, aerorsync_serve, ...) absent
# from the universal dir and breaking `.app` bundling. A real target
# triple builds and bundles every bin natively in one pass. The old
# macos-13 (Intel) leg was removed: GitHub retired that runner image
# on 2025-12-08, leaving the job queued forever.
- platform: macos-14
target: macos
arch: aarch64
- platform: macos-14
target: macos
arch: x64
runs-on: ${{ matrix.platform }}
steps:
- name: Free disk space (Linux)
if: matrix.target == 'linux'
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/share/swift
sudo apt-get clean
df -h /
- name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Setup Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: '20'
cache: 'npm'
- name: Install Rust
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: stable
- name: Rust cache
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
with:
workspaces: './src-tauri -> target'
# Cache prefix bumped 2026-05-06 to invalidate any cached
# whisper-rs-sys CMakeCache.txt referencing Visual Studio 17 2022
# after the windows-latest runner image rolled to VS 18 2026.
# See "Drop stale whisper-rs-sys CMake artifacts" below for the
# belt-and-suspenders runtime cleanup.
prefix-key: 'v2-whisper-vs18'
# PRs restore; only main saves (Actions cache quota is 10 GB).
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Add macOS Intel cross target
if: matrix.target == 'macos' && matrix.arch == 'x64'
run: rustup target add x86_64-apple-darwin
# whisper-rs-sys vendors whisper.cpp and runs CMake at build time. The
# generator picked by `cmake-rs` is whichever Visual Studio CMake finds
# on the runner. When GitHub rolls the windows-latest image to a newer
# VS (e.g. VS 17 2022 -> VS 18 2026 preview), the rust-cache restored
# CMakeCache.txt still references the previous generator and CMake
# aborts: "generator does not match the generator used previously".
# Belt-and-suspenders: even with the prefix-key bump we drop the
# whole whisper-rs-sys-* tree on Windows to force a clean reconfigure,
# in case a future cache key collision restores stale artifacts.
# Removing only `out/` previously left `out/build/CMakeCache.txt`
# behind on some runners depending on cmake-rs internal layout, so we
# nuke the entire crate build dir.
- name: Drop stale whisper-rs-sys CMake artifacts (Windows)
if: matrix.target == 'windows'
shell: pwsh
run: |
$base = "src-tauri/target/release/build"
if (Test-Path $base) {
$purged = 0
Get-ChildItem $base -Directory -ErrorAction SilentlyContinue |
Where-Object { $_.Name -like 'whisper-rs-sys-*' } |
ForEach-Object {
Write-Host "Removing whole whisper-rs-sys build dir: $($_.FullName)"
Remove-Item -Recurse -Force $_.FullName -ErrorAction SilentlyContinue
$purged++
}
Write-Host "Purged $purged whisper-rs-sys build dir(s)"
} else {
Write-Host "No prior build dir, nothing to purge"
}
- name: Install Linux dependencies
if: matrix.target == 'linux'
run: |
# Runner-image extras: Microsoft's apt repos 403 on their InRelease often
# enough to redden a run, and nothing here installs from them.
sudo rm -f /etc/apt/sources.list.d/microsoft* /etc/apt/sources.list.d/azure-cli*
sudo apt-get update
# `dbus` lives on `linux-validate` with `cargo test` / portal_chooser
# (#510). This leg only needs the Tauri/bundle toolchain.
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libfuse3-dev libacl1-dev libacl1 binutils rpm squashfs-tools
# No snapcraft here any more: this leg stopped building the snap when
# PR #172 moved it to its own job, and snapcore/action-build brings
# its own snapcraft. The leftover install (5 retries, 60 s apart on a
# store hiccup) was pure cost on every PR run.
- name: Install npm dependencies
run: npm ci
- name: Run security regression suite (MVP)
run: npm run security:regression
# Native build: Linux, Windows, and the macOS arm64 leg (host arch, no
# --target). A real/native target dir keeps every package bin in
# target/release so the bundler finds them all.
#
# Linux AppImage packaging downloads AppRun/linuxdeploy helpers from
# GitHub during Tauri's bundling phase. GitHub-hosted runners can drop
# those connections after the Rust build is already complete, so retry
# only the Linux build step and clear partial helper downloads between
# attempts.
- name: Build Tauri app (Linux with AppImage download retry)
if: matrix.target == 'linux'
shell: bash
run: |
set -u
for attempt in 1 2 3; do
if npm run tauri build; then
exit 0
fi
status=$?
if [ "$attempt" -eq 3 ]; then
exit "$status"
fi
echo "::warning::Tauri Linux build failed on attempt ${attempt}/3; retrying after clearing partial AppImage helper downloads"
find src-tauri/target/release/bundle/appimage \
-maxdepth 2 \
\( -name 'AppRun*' -o -name 'linuxdeploy*' \) \
-type f -delete 2>/dev/null || true
sleep $((attempt * 20))
done
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# The snap is packaged in the dedicated `build-snap` job below, but its
# payload MUST be compiled on the same Ubuntu release as the snap's
# `base:` - core22 means jammy, i.e. THIS ubuntu-22.04 leg. Between
# 2026-05-06 (PR #172) and 2026-07-25 the snap job compiled its own
# payload on ubuntu-24.04, so ~30 published revisions demanded
# GLIBC_2.38/2.39 from a 2.35 runtime and died at exec with
# "version 'GLIBC_2.38' not found" - the app could not start at all.
# See issue #460 for the full analysis.
#
# Handing the jammy binaries to the snap job means .deb/.rpm/.AppImage
# and .snap all ship the exact same bytes, so the mismatch cannot return.
# Tarred because GitHub artifacts do not preserve the executable bit and
# snapcraft's override-build copies these binaries verbatim.
- name: Package Linux payload for the snap job
if: matrix.target == 'linux' && (startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch')
shell: bash
run: |
set -euo pipefail
cd src-tauri/target/release
PAYLOAD=(aeroftp aeroftp-dispatch-bundle/aeroftp-cli aeroftp-dispatch-bundle/aeroftp-dispatch)
for f in "${PAYLOAD[@]}"; do
if [ ! -f "$f" ]; then
echo "::error::missing payload binary $f - the snap job cannot be fed"
exit 1
fi
done
# Staged inside the workspace, not in RUNNER_TEMP: an artifact whose
# source lies outside the workspace is stored under a relative path
# that `gh run download` rejects as path traversal, which is exactly
# when you need it - debugging a failed release from a laptop.
mkdir -p "$GITHUB_WORKSPACE/payload-stage"
tar -cf "$GITHUB_WORKSPACE/payload-stage/linux-payload.tar" "${PAYLOAD[@]}"
ls -l "$GITHUB_WORKSPACE/payload-stage/linux-payload.tar"
- name: Upload Linux payload for the snap job
if: matrix.target == 'linux' && (startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch')
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: aeroftp-linux-payload-${{ github.sha }}
path: payload-stage/linux-payload.tar
if-no-files-found: error
retention-days: 1
- name: Build Tauri app
if: matrix.target != 'linux' && (matrix.target != 'macos' || matrix.arch == 'aarch64')
run: npm run tauri build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# macOS Intel: cross-compile to the real x86_64-apple-darwin triple on
# the arm64 runner. cargo builds AND tauri bundles every bin natively
# under target/x86_64-apple-darwin/release (no universal pseudo-target,
# no lipo, no missing-bin bundle failures).
- name: Build Tauri app (macOS Intel)
if: matrix.target == 'macos' && matrix.arch == 'x64'
run: npm run tauri build -- --target x86_64-apple-darwin
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# The Intel cross build emits its bundle under the target-triple dir.
# Stage the DMG at the conventional path so every downstream step
# (artifact upload, Sigstore signing, release upload)
# keeps working unchanged. The arm64 leg already builds there.
- name: Stage Intel DMG at conventional path
if: matrix.target == 'macos' && matrix.arch == 'x64'
shell: bash
run: |
set -euo pipefail
mkdir -p src-tauri/target/release/bundle/dmg
cp src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/*.dmg \
src-tauri/target/release/bundle/dmg/
ls -la src-tauri/target/release/bundle/dmg/
- name: Patch Debian dispatcher layout
if: matrix.target == 'linux'
shell: bash
run: |
shopt -s nullglob
for deb in src-tauri/target/release/bundle/deb/*.deb; do
src-tauri/scripts/patch-deb-dispatch.sh "$deb"
done
# R13 (APPENDIX-CLI-DISPATCH): record the size footprint of the
# dispatcher payload and the produced bundles so regressions in
# binary size (e.g. an accidental dependency that pulls in a heavy
# transitive crate) surface in the CI log. Informational only: no
# fail conditions; we want the diff visible in PR run logs, not a
# hard gate. Linux-only because the dispatcher + bundle layout is
# Linux-specific; Windows packaging will get its own report when
# CD-3.PACK lands.
- name: R13 size report (dispatcher payload + bundles)
if: matrix.target == 'linux'
shell: bash
run: |
echo "=== Release binary sizes ==="
for bin in aeroftp aeroftp-cli aeroftp-dispatch aerorsync_serve; do
f="src-tauri/target/release/$bin"
if [ -f "$f" ]; then
size=$(stat -c '%s' "$f")
human=$(numfmt --to=iec --suffix=B "$size")
printf ' %-22s %10s (%s bytes)\n' "$bin" "$human" "$size"
fi
done
echo
echo "=== Bundle artifact sizes ==="
for f in \
src-tauri/target/release/bundle/deb/*.deb \
src-tauri/target/release/bundle/rpm/*.rpm \
src-tauri/target/release/bundle/appimage/*.AppImage; do
if [ -f "$f" ]; then
size=$(stat -c '%s' "$f")
human=$(numfmt --to=iec --suffix=B "$size")
printf ' %-60s %10s\n' "$(basename "$f")" "$human"
fi
done
# Fix Tauri AppImage for cross-distro compatibility (issue #90).
#
# Problem: linuxdeploy bundles shared libraries (WebKitGTK, GStreamer, etc.)
# compiled on Ubuntu. On Arch/Fedora/rolling distros these bundled libs
# conflict with system EGL/Mesa/libavif/GStreamer, causing cascading
# "undefined symbol" errors and "EGL_BAD_PARAMETER" blank screens.
#
# Fix: remove ALL bundled shared libraries. The AppImage uses system libs
# at runtime. Tested on Arch (EndeavourOS) + KDE Wayland + Intel Haswell.
# The binary's C API linkage is stable across distros: only the bundled
# libs caused ABI conflicts. The user must have webkit2gtk-4.1 installed
# (same requirement as the .deb and AUR packages).
#
# Also fixes .DirIcon absolute symlink (AppImageHub spec compliance).
- name: Fix AppImage (unbundle libs + DirIcon)
if: matrix.target == 'linux'
run: |
APPIMAGE=$(realpath src-tauri/target/release/bundle/appimage/*.AppImage 2>/dev/null | head -1)
if [ -z "$APPIMAGE" ]; then echo "No AppImage found, skipping"; exit 0; fi
echo "Patching $APPIMAGE"
chmod +x "$APPIMAGE"
ORIG_SIZE=$(du -m "$APPIMAGE" | cut -f1)
# Extract squashfs
OFFSET=$("$APPIMAGE" --appimage-offset)
mkdir -p /tmp/appimage-fix
unsquashfs -d /tmp/appimage-fix/squashfs-root -o "$OFFSET" "$APPIMAGE"
cd /tmp/appimage-fix/squashfs-root
# --- Fix 1: .DirIcon absolute symlink ---
rm -f .DirIcon
ICON=$(ls *.png 2>/dev/null | head -1)
if [ -n "$ICON" ]; then
ln -s "$ICON" .DirIcon
echo "Fixed: .DirIcon -> $ICON (relative)"
fi
# --- Fix 2: dispatcher layout ---
"$GITHUB_WORKSPACE/src-tauri/scripts/patch-appimage-dispatch.sh" "$(pwd)" "$GITHUB_WORKSPACE"
# --- Fix 3: Remove all bundled shared libraries (issue #90) ---
# Keeps: binary, icons, desktop file, AppRun, metadata.
# Removes: usr/lib/*.so* (WebKitGTK, GStreamer, GLib, etc.)
LIB_COUNT=$(find usr/lib -name '*.so*' -type f -o -name '*.so*' -type l 2>/dev/null | wc -l)
rm -f usr/lib/lib*.so*
echo "Removed $LIB_COUNT bundled shared libraries"
# --- Re-package using appimagetool ---
cd /tmp/appimage-fix
APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
APPIMAGETOOL_SHA256="a6d71e2b6cd66f8e8d16c37ad164658985e0cf5fcaa950c90a482890cb9d13e0"
wget -q "$APPIMAGETOOL_URL" -O appimagetool
echo "${APPIMAGETOOL_SHA256} appimagetool" | sha256sum -c - || {
echo "::error::appimagetool checksum mismatch: possible supply chain compromise. Update APPIMAGETOOL_SHA256 in build.yml after verification."
echo "Actual checksum: $(sha256sum appimagetool | cut -d' ' -f1)"
exit 1
}
chmod +x appimagetool
ARCH=x86_64 ./appimagetool --appimage-extract-and-run squashfs-root "$APPIMAGE"
NEW_SIZE=$(du -m "$APPIMAGE" | cut -f1)
echo "AppImage re-packaged: ${ORIG_SIZE}MB -> ${NEW_SIZE}MB (system libs only)"
- name: Assert Linux ACL packaging contracts
if: matrix.target == 'linux'
shell: bash
run: |
chmod +x scripts/assert-linux-acl-packaging.sh
./scripts/assert-linux-acl-packaging.sh --require-bundles
# Snap build moved to dedicated `build-snap` job below (issue #164).
# The Snap Store stayed frozen at 3.6.10 because the inline step kept
# silent-OOM-ing during LXD setup on the ubuntu-22.04 matrix runner.
# Splitting it onto its own ubuntu-24.04 runner with native LXD 5.21+
# gives Snap headroom and lets us drop continue-on-error once stable.
# Create portable ZIP for Windows (no installer needed).
# Includes:
# - AeroFTP.exe (the app)
# - aeroftp-cli.exe (headless CLI: providers, transfers, vault)
# - aeroftp-dispatch.exe (CLI subcommand dispatcher / shim)
# - portable.marker (detection key for the in-place auto-updater;
# presence triggers portable-mode behaviour:
# data dir next to .exe, in-place .exe swap on
# update instead of NSIS installer launch)
# - README.txt (user-facing quick-start + uninstall + updates)
# - LICENSE.txt (GPLv3, copied from the repo root)
- name: Create Windows portable ZIP
if: matrix.target == 'windows'
shell: pwsh
run: |
$exe = "src-tauri/target/release/AeroFTP.exe"
if (-not (Test-Path $exe)) {
Write-Host "::error::AeroFTP.exe not found at $exe"
exit 1
}
# The CLI binaries are produced by the same `tauri build` (cargo builds
# every [[bin]] in the crate). Ship them inside the portable ZIP so the
# headless CLI is available without the installer (issue #340). Hard-fail
# if absent so a packaging regression can never silently drop them again.
$cliExe = "src-tauri/target/release/aeroftp-cli.exe"
$dispatchExe = "src-tauri/target/release/aeroftp-dispatch.exe"
foreach ($bin in @($cliExe, $dispatchExe)) {
if (-not (Test-Path $bin)) {
Write-Host "::error::CLI binary not found at $bin"
exit 1
}
}
$version = (Get-Content src-tauri/tauri.conf.json | ConvertFrom-Json).version
$zipName = "AeroFTP-${version}-portable-windows-x64.zip"
$stage = "src-tauri/target/release/bundle/portable-stage"
# Stage the portable layout in a clean temp dir so Compress-Archive
# captures the structure exactly as the user will see it after extraction.
if (Test-Path $stage) { Remove-Item -Recurse -Force $stage }
New-Item -ItemType Directory -Path $stage | Out-Null
Copy-Item $exe "$stage\AeroFTP.exe"
Copy-Item $cliExe "$stage\aeroftp-cli.exe"
Copy-Item $dispatchExe "$stage\aeroftp-dispatch.exe"
Copy-Item "src-tauri/installer/portable/portable.marker" "$stage\portable.marker"
Copy-Item "src-tauri/installer/portable/README.txt" "$stage\README.txt"
Copy-Item "LICENSE" "$stage\LICENSE.txt"
$dest = "src-tauri/target/release/bundle/$zipName"
if (Test-Path $dest) { Remove-Item -Force $dest }
Compress-Archive -Path "$stage\*" -DestinationPath $dest
$size = (Get-Item $dest).Length
Write-Host "Created portable ZIP: $zipName ($size bytes)"
Write-Host "Contents:"
Get-ChildItem $stage | ForEach-Object { Write-Host " $($_.Name) ($($_.Length) bytes)" }
# Always upload platform installers as workflow artifacts. Tag runs used
# to skip this and publish straight from the runner; that raced
# linux-validate after C2. Tags now publish from these artifacts in
# `publish-github-release`, which needs linux-validate first.
- name: Upload Linux installers
if: matrix.target == 'linux'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: aeroftp-linux-${{ github.sha }}
path: |
src-tauri/target/release/bundle/deb/*.deb
src-tauri/target/release/bundle/rpm/*.rpm
src-tauri/target/release/bundle/appimage/*.AppImage
if-no-files-found: warn
retention-days: 1
- name: Upload Windows installers
if: matrix.target == 'windows'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: aeroftp-windows-${{ github.sha }}
path: |
src-tauri/target/release/bundle/msi/*.msi
src-tauri/target/release/bundle/nsis/*.exe
src-tauri/target/release/bundle/AeroFTP-*-portable-*.zip
if-no-files-found: warn
retention-days: 1
- name: Upload macOS installers
if: matrix.target == 'macos'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: aeroftp-macos-${{ matrix.arch }}-${{ github.sha }}
path: src-tauri/target/release/bundle/dmg/*.dmg
if-no-files-found: warn
retention-days: 1
# Tag publication is a separate job so a v* tag cannot sign/upload while
# linux-validate is still red. Packaging stays parallel with validation.
# `!cancelled()` plus an explicit linux-validate success check matches
# build-snap: a flaky Windows/macOS matrix leg must not block Linux assets,
# but a failed test/clippy job must.
publish-github-release:
if: ${{ !cancelled() && startsWith(github.ref, 'refs/tags/') && needs.linux-validate.result == 'success' }}
needs: [linux-validate, build-and-release]
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Download Linux installers
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: aeroftp-linux-${{ github.sha }}
path: release-assets
- name: Download Windows installers
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: aeroftp-windows-${{ github.sha }}
path: release-assets
- name: Download macOS aarch64 installer
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: aeroftp-macos-aarch64-${{ github.sha }}
path: release-assets
- name: Download macOS x64 installer
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: aeroftp-macos-x64-${{ github.sha }}
path: release-assets
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Sign release artifacts with Sigstore
shell: bash
run: |
set -euo pipefail
shopt -s nullglob
mkdir -p release-assets
mapfile -t ARTIFACTS < <(find release-assets -type f ! -name '*.sigstore.json' | sort)
echo "Found ${#ARTIFACTS[@]} artifacts to sign"
if [ "${#ARTIFACTS[@]}" -eq 0 ]; then
echo "::error::no packaging artifacts to sign; linux-validate passed but the matrix uploaded nothing"
exit 1
fi
for artifact in "${ARTIFACTS[@]}"; do
echo "Signing: $(basename "$artifact")"
cosign sign-blob \
--bundle "${artifact}.sigstore.json" \
--new-bundle-format \
--yes \
"$artifact"
echo "Signed: $(basename "$artifact").sigstore.json"
done
- name: Extract changelog for release
id: changelog
run: |
VERSION="${GITHUB_REF_NAME#v}"
NOTES=$(sed -n "/^## \[${VERSION}\]/,/^## \[/p" CHANGELOG.md | head -n -1)
NOTES="${NOTES}
**Downloads:**
- **Windows**: \`.msi\` installer, \`.exe\`, or \`.zip\` portable (no installation required)
- **macOS**: \`.dmg\` disk image
- **Linux**: \`.deb\`, \`.rpm\`, \`.snap\`, or \`.AppImage\`"
echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "$NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Upload binaries to GitHub Release
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
with:
name: 'AeroFTP ${{ github.ref_name }}'
body: ${{ steps.changelog.outputs.notes }}
draft: false
prerelease: false
# Published release assets are immutable. The action defaults to overwriting,
# so re-running a tag build silently replaced every asset with a fresh,
# non-reproducible build - which is how the winget 3.3.5 manifest ended up
# pinning digests that no longer existed (microsoft/winget-pkgs#407672).
# With false the action skips assets that are already there and only uploads
# the missing ones, so a re-run repairs a partial release instead of
# invalidating a complete one. Replacing an asset is now a deliberate manual
# delete, not a side effect.
overwrite_files: false
files: |
release-assets/**
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# .snap upload is handled in the dedicated build-snap job (issue #164)
# Build the Snap package on a dedicated runner. Issue #164 background:
# the original inline step was silent-OOM-ing during LXD setup on the
# ubuntu-22.04 matrix runner. Splitting it out (this job) gives Snap a
# fresh ubuntu-24.04 runner with native LXD 5.21+ headroom.
#
# NOTE 2026-05-06 (v3.7.3): the v3.7.2 deploy of this job produced
# zero-byte snaps because `snap/snapcraft.yaml`'s `override-build` step
# copies a *pre-built* `src-tauri/target/release/aeroftp` binary into
# the snap layout. The original inline step inherited that binary from
# the matrix Linux job's `npm run tauri build`; the dedicated job did not,
# so it reproduced the whole pre-build (Node + Rust + `npm run tauri build`)
# here instead.
#
# NOTE 2026-07-26: that pre-build was the bug. It ran on THIS ubuntu-24.04
# runner (glibc 2.39) while `snap/snapcraft.yaml` stayed on `base: core22`
# (glibc 2.35), so from v3.7.2 to v4.1.6 every published revision shipped a
# payload the core22 loader refuses - "version 'GLIBC_2.38' not found" - and
# the app could not start at all. ~30 releases, caught by nobody because the
# only assertion was "the .snap exists and is >10 MB".
#
# The job is now packaging-only: it consumes the jammy binaries built by the
# ubuntu-22.04 `linux` leg, so the snap ships the same bytes as the
# .deb/.rpm/.AppImage and the build environment matches the base by
# construction. Two blocking gates below prove it before anything is
# published. Full analysis: issue #460.
#
# `needs:` is the point, not a detail: the payload must come from the jammy
# leg. It costs ~15 min of release wall clock and saves a whole duplicate
# Rust build (~60 min of runner time).
#
# Triggers:
# - Tag pushes: full pipeline (build + sign + release upload + Snap Store)
# - workflow_dispatch: build only (validation runs without cutting a tag)
build-snap:
# `!cancelled()` rather than the implicit success(): the snap only truly
# depends on the ubuntu-22.04 leg's payload, so a flaky macOS or Windows leg
# must not block it. If the Linux leg is the one that failed, the payload
# artifact is missing and this job fails on the download - which is right.
if: ${{ !cancelled() && needs.linux-validate.result == 'success' && (startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch') }}
needs: [linux-validate, build-and-release]
runs-on: ubuntu-24.04
permissions:
contents: write # required for softprops/action-gh-release
id-token: write # required for Sigstore keyless signing via OIDC
steps:
- name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
# Guard the snap version BEFORE packaging. Kept even though this job now
# runs behind `needs: [linux-validate, build-and-release]` (R10 lives on
# linux-validate): it costs a second, and it is what caught v4.0.9 shipping a
# Sigstore-signed 4.0.8 snap as an asset of the 4.0.9 release. The
# snap-refresh rebuild path carries its own copy of this guard. Parsed
# with grep/sed because there is no node in this job any more.
- name: R10-snap version guard (snapcraft.yaml vs tag)
shell: bash
run: |
SNAP=$(grep -m1 '^version:' snap/snapcraft.yaml | sed "s/.*['\"]\(.*\)['\"].*/\1/")
echo "snapcraft.yaml: $SNAP"
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
TAG="${GITHUB_REF_NAME#v}"
echo "tag: $TAG"
if [ "$SNAP" != "$TAG" ]; then
echo "::error::snap version drift: snapcraft.yaml=$SNAP does not match tag=$TAG"
exit 1
fi
else
PKG=$(grep -m1 '^ "version"' package.json | sed 's/.*: *"\([^"]*\)".*/\1/')
echo "package.json: $PKG"
if [ -n "$PKG" ] && [ "$SNAP" != "$PKG" ]; then
echo "::error::snap version drift: snapcraft.yaml=$SNAP does not match package.json=$PKG"
exit 1
fi
fi
# The payload comes from the ubuntu-22.04 leg, never from this runner:
# jammy binaries for a jammy (core22) base. Nothing here compiles Rust.
- name: Download Linux payload built on jammy
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: aeroftp-linux-payload-${{ github.sha }}
path: ${{ runner.temp }}/payload
- name: Restore payload into src-tauri/target/release
shell: bash
run: |
set -euo pipefail
mkdir -p src-tauri/target/release
tar -xf "$RUNNER_TEMP/payload/linux-payload.tar" -C src-tauri/target/release
cd src-tauri/target/release
# The executable bit does not survive a GitHub artifact round-trip in
# every path, and snapcraft copies these binaries as-is.
chmod +x aeroftp aeroftp-dispatch-bundle/aeroftp-cli aeroftp-dispatch-bundle/aeroftp-dispatch
for f in aeroftp aeroftp-dispatch-bundle/aeroftp-cli aeroftp-dispatch-bundle/aeroftp-dispatch; do
[ -x "$f" ] || { echo "::error::payload binary $f missing or not executable"; exit 1; }
echo " $(ls -l "$f")"
done
# snapcore/action-build@3bdaa03e is the latest pin (no v2 exists,
# see issue #164 for the analysis). On ubuntu-24.04 the native LXD
# is 5.21+ which clears the silent OOM observed on 22.04. No
# continue-on-error: a snap build failure must block the release
# like deb / rpm / AppImage, per the closing acceptance criterion
# of #164.
- name: Build Snap package
id: snap-build
uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1
- name: Confirm Snap was built
id: snap-check
shell: bash
run: |
SNAP="${{ steps.snap-build.outputs.snap }}"
if [ -z "$SNAP" ] || [ ! -f "$SNAP" ]; then
echo "::error::Snap file not produced by snapcore/action-build"
# Single-quoted on purpose: inside double quotes bash would run the
# backticked text as a command and mangle its own error message.
echo 'Expected output steps.snap-build.outputs.snap was empty or missing on disk.'
exit 1
fi
BYTES=$(stat -c %s "$SNAP")
if [ "$BYTES" -lt 10485760 ]; then
echo "::error::Snap file is suspiciously small ($BYTES bytes)"
exit 1
fi
echo "Snap produced: $SNAP ($(du -h "$SNAP" | cut -f1))"
echo "snap=$SNAP" >> "$GITHUB_OUTPUT"
# Uploaded BEFORE the gates on purpose: when a gate rejects a snap, the
# rejected artifact is exactly what you need in hand to diagnose it.
# Publishing still happens only after both gates pass.
- name: Upload Snap as workflow artifact (validation runs)
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: aeroftp-snap-${{ github.sha }}
path: ${{ steps.snap-check.outputs.snap }}
if-no-files-found: error
retention-days: 1
# ----------------------------------------------------------------------
# Gate G1 - static ABI floor. Every ELF in the snap must fit the glibc the
# `base:` snap provides; the ceiling is read from the base itself, never
# hardcoded. This is the check whose absence let ~30 unstartable revisions
# reach the Snap Store between 2026-05-06 and 2026-07-25.
# ----------------------------------------------------------------------
- name: Gate G1 - snap ABI floor vs base
shell: bash
env:
SNAP_FILE: ${{ steps.snap-check.outputs.snap }}
run: |
# Runner-image extras: Microsoft's apt repos 403 on their InRelease often
# enough to redden a run, and nothing here installs from them.
sudo rm -f /etc/apt/sources.list.d/microsoft* /etc/apt/sources.list.d/azure-cli*
sudo apt-get update
sudo apt-get install -y --no-install-recommends squashfs-tools binutils file
./scripts/snap-abi-check.sh "$SNAP_FILE"
# Full GPU-userspace gate (libgbm, vendor libdrm, vulkan ICDs, Mesa
# loaders, snapcraft gpu: lint). The two-pattern inline grep this
# replaces missed everything except the loudest DRI/EGL paths; the
# weekly snap-refresh job already ran the script, the tag-driven
# release pipeline must too.
./scripts/snap-gpu-lint-check.sh "$SNAP_FILE"
echo "OK: GPU userspace is supplied only by graphics-core22."
chmod +x "$GITHUB_WORKSPACE/scripts/assert-linux-acl-packaging.sh"
SNAP_FILE="$SNAP_FILE" "$GITHUB_WORKSPACE/scripts/assert-linux-acl-packaging.sh" --snap "$SNAP_FILE"
# ----------------------------------------------------------------------
# Gate G2 - install the snap and run a command that actually reaches the
# payload. `aeroftp --version` is NOT a valid smoke test: a leading dash
# routes to DispatchRoute::Gui (needs a display), and the dispatcher can
# answer without the payload ever loading. Only a token from
# CLI_SUBCOMMANDS (src-tauri/src/cli_dispatch.rs) execs aeroftp-cli and
# therefore proves the payload links against the base at runtime.
# ----------------------------------------------------------------------
- name: Install the graphics userspace provider
shell: bash
run: sudo snap install mesa-core22
- name: Gate G2 - install the snap and exercise the payload
shell: bash
env:
SNAP_FILE: ${{ steps.snap-check.outputs.snap }}
run: |
set -euo pipefail
sudo snap install --dangerous "$SNAP_FILE"
sudo snap connect aeroftp:graphics-core22 mesa-core22:graphics-core22
echo "--- aeroftp ls --help (routes to the CLI payload) ---"
out="$(/snap/bin/aeroftp ls --help 2>&1)" || {
echo "::error::the installed snap cannot run 'aeroftp ls --help'"
echo "$out"
exit 1
}
echo "$out" | head -20
if ! grep -qiE 'usage|aeroftp' <<<"$out"; then
echo "::error::unexpected output from the snap CLI payload"
exit 1
fi
sudo snap remove --purge aeroftp || true
# ----------------------------------------------------------------------
# Gate G3 - launch the actual confined GUI on a disposable X server.
# This checks the frontend readiness log, not merely process liveness,
# and catches missing EGL/GLVND wiring such as issue #462.
# ----------------------------------------------------------------------
- name: Gate G3 - confined GUI readiness under Xvfb
shell: bash
env:
SNAP_FILE: ${{ steps.snap-check.outputs.snap }}
SNAP_GUI_SCREENSHOT: ${{ runner.temp }}/snap-gui-screenshot.png
run: |
set -euo pipefail
# Runner-image extras: Microsoft's apt repos 403 on their InRelease often
# enough to redden a run, and nothing here installs from them.
sudo rm -f /etc/apt/sources.list.d/microsoft* /etc/apt/sources.list.d/azure-cli*
sudo apt-get update
sudo apt-get install -y --no-install-recommends xvfb x11-apps imagemagick
sudo snap install --dangerous "$SNAP_FILE"
trap 'sudo snap remove --purge aeroftp || true' EXIT
sudo snap connect aeroftp:graphics-core22 mesa-core22:graphics-core22
./scripts/snap-gui-check.sh
# The captured frame is the only human-reviewable evidence that the window
# is not the blank one from #462; keep it on failure above all.
- name: Upload the confined GUI frame
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: snap-gui-frame-${{ github.sha }}
path: ${{ runner.temp }}/snap-gui-screenshot.png
if-no-files-found: warn
retention-days: 7
# Sigstore keyless signing of the .snap: only on tags.
- name: Install Cosign
if: startsWith(github.ref, 'refs/tags/')
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Sign Snap artifact with Sigstore
if: startsWith(github.ref, 'refs/tags/')
shell: bash
run: |
SNAP="${{ steps.snap-check.outputs.snap }}"
echo "Signing: $(basename "$SNAP")"
cosign sign-blob \
--bundle "${SNAP}.sigstore.json" \
--new-bundle-format \
--yes \
"$SNAP"
echo "Signed: $(basename "$SNAP").sigstore.json"
# Upload .snap (and Sigstore bundle) to the GitHub Release additively.
# This runs after build-and-release has already created the release
# with deb/rpm/AppImage; softprops/action-gh-release with the same
# tag and `name` simply appends the new files.
- name: Upload Snap to GitHub Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
with:
name: 'AeroFTP ${{ github.ref_name }}'
draft: false
prerelease: false
# See the Linux upload step: published assets are immutable.
overwrite_files: false
files: |
${{ steps.snap-check.outputs.snap }}
${{ steps.snap-check.outputs.snap }}.sigstore.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Publish to Snap Store (stable channel). Kept on continue-on-error
# because a transient Snap Store outage should not retroactively
# fail the GitHub Release we already published above; the .snap is
# still available as a release asset for manual upload if needed.
- name: Publish to Snap Store
if: startsWith(github.ref, 'refs/tags/')
continue-on-error: true
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
run: |
SNAP="${{ steps.snap-check.outputs.snap }}"
# snapcraft was installed by snapcore/action-build via LXD; we need
# the plain CLI here for `upload --release`.
if ! command -v snapcraft >/dev/null 2>&1; then
sudo snap install snapcraft --classic
fi
echo "Uploading $SNAP to Snap Store stable..."
snapcraft upload --release=stable "$SNAP"
# Publish to Windows Package Manager (winget) on tag pushes
publish-winget:
needs: [linux-validate, build-and-release]
if: ${{ startsWith(github.ref, 'refs/tags/') && needs.linux-validate.result == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Publish to WinGet
# A8-06: Pinned to SHA (v2 tag as of 2026-03-07)
uses: vedantmgoyal9/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2
with:
identifier: axpnet.AeroFTP
# This regex MUST match exactly the installer set of the previously published
# manifest. komac builds the new version on the shape of the previous one, so a
# regex matching fewer assets than there are installer slots makes it fill the
# spare slots with a duplicate -> DuplicateInstallerEntry (hit at 4.1.5 in
# microsoft/winget-pkgs#402974 and again at 4.1.6 in #407642, when this was
# narrowed to the exe alone to dodge a then-failing wix validation lane).
# The package publishes two installers: x64 NSIS user-scope and x64 WiX
# machine-scope. Dropping the msi here also drops the machine-scope upgrade path.
installers-regex: '_x64-setup\.exe$|_x64_en-US\.msi$'
fork-user: axpnet
token: ${{ secrets.WINGET_TOKEN }}