-
Notifications
You must be signed in to change notification settings - Fork 10
1078 lines (1039 loc) · 52.5 KB
/
Copy pathrelease.yml
File metadata and controls
1078 lines (1039 loc) · 52.5 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
# Streaming release: the GitHub release is created up front, then every job
# uploads ITS artifact the moment it's built (gh release upload --clobber).
# A slow leg (aarch64/i686 under QEMU, or the windows vcpkg build) never blocks
# the release or the artifacts that are already done.
#
# Triggers:
# • pushing a tag `vX.Y.Z` → builds that tag, publishes the release
# • manual run (workflow_dispatch) → builds the chosen ref, publishes `tag`
#
# Artifacts: agentty-linux-{x86_64,aarch64,i686}, agentty-macos-{arm64,x86_64},
# agentty-windows-x86_64.exe, agentty-windows-x86_64.msi (signed installer),
# agentty_<v>_{amd64,arm64}.deb, agentty-<v>-1.{x86_64,aarch64}.rpm,
# agentty-bin-<v>-1-x86_64.pkg.tar.zst, agentty-<v>.tar.gz, SHA256SUMS.
#
# Downstream publishing (all gated on a real tag + a per-target secret; each
# skips silently when its secret is absent):
# winget → PR to microsoft/winget-pkgs (WINGET_TOKEN)
# homebrew→ PR/commit to the 1ay1/homebrew-tap (TAP_TOKEN)
# scoop → PR/commit to the 1ay1/scoop-bucket (SCOOP_TOKEN)
# AUR → push agentty-bin to the AUR (AUR_SSH_KEY)
# alpine → .apk built + version-pinned APKBUILD attached to the release
# nix/snap/gentoo → version-pinned manifests attached to the release for
# manual/overlay publishing (nixpkgs/snapcraft/overlay PRs)
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Release tag (blank → v<CMake project version>)'
required: false
default: ''
permissions:
contents: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
env:
GH_TOKEN: ${{ github.token }}
jobs:
# Create/refresh the release immediately + upload the source tarball.
prepare:
name: prepare release
runs-on: ubuntu-latest
outputs:
version: ${{ steps.meta.outputs.version }}
tag: ${{ steps.meta.outputs.tag }}
steps:
- uses: actions/checkout@v5
- name: Init submodules (HTTPS, race-tolerant)
run: |
git config --global url."https://github.com/".insteadOf "git@github.com:"
for attempt in 1 2 3 4 5; do
if git submodule update --init --recursive --force; then break; fi
echo "submodule update failed (attempt $attempt) — full-fetching and retrying"
git submodule sync --recursive
git submodule foreach --recursive 'git fetch --prune origin || true'
sleep $((attempt * 10))
done
git submodule update --init --recursive --force
- name: Resolve version + tag
id: meta
run: |
V=$(grep -m1 'project(agentty' CMakeLists.txt | sed -E 's/.*VERSION ([0-9.]+).*/\1/')
echo "version=$V" >> "$GITHUB_OUTPUT"
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
# Blank input → derive the tag from the CMake project version.
TAG="${{ inputs.tag }}"
[ -n "$TAG" ] || TAG="v$V"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
else
echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
fi
- name: Ensure release exists + upload source tarball
env:
V: ${{ steps.meta.outputs.version }}
TAG: ${{ steps.meta.outputs.tag }}
run: |
# Create the release if it doesn't exist. IMPORTANT: `gh release
# view` succeeds on a DRAFT too — a draft left by a prior cancelled
# run (or auto-created on tag push) would otherwise absorb every
# upload while staying invisible (public download URLs 404, unauth
# API omits it). So after ensuring it exists, ALWAYS force it
# published + non-prerelease. This is the fix for "run succeeded but
# assets 404 / release doesn't show up".
gh release view "$TAG" >/dev/null 2>&1 \
|| gh release create "$TAG" --title "$TAG" \
--notes "See [CHANGELOG.md](https://github.com/1ay1/agentty/blob/master/CHANGELOG.md)."
gh release edit "$TAG" --draft=false --prerelease=false
pipx run git-archive-all "agentty-${V}.tar.gz"
gh release upload "$TAG" "agentty-${V}.tar.gz" --clobber
# Linux static (musl) binaries — one job per arch, NOT a matrix. A matrix
# job only reports complete when its slowest leg finishes (aarch64 under
# QEMU, ~13 min), and `needs:` can't target a single matrix leg — so a
# matrix would re-gate the fast packages behind the slow build, and a failed
# aarch64 leg would block x86 packaging entirely. Separate jobs let the x86
# binary + packages publish in ~5 min while aarch64 streams in later, and
# isolate each arch's failures from the others.
build-linux-x86_64:
name: linux x86_64
needs: prepare
runs-on: ubuntu-latest
env:
TAG: ${{ needs.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v5
- name: Init submodules (HTTPS, race-tolerant)
run: |
git config --global url."https://github.com/".insteadOf "git@github.com:"
for attempt in 1 2 3 4 5; do
if git submodule update --init --recursive --force; then break; fi
echo "submodule update failed (attempt $attempt) — full-fetching and retrying"
git submodule sync --recursive
git submodule foreach --recursive 'git fetch --prune origin || true'
sleep $((attempt * 10))
done
git submodule update --init --recursive --force
- name: Build static binary (Alpine 3.21 / musl / GCC 14)
run: |
docker run --rm --platform linux/amd64 \
-e CMARCH='avx2' \
-v "$PWD":/src -w /src alpine:3.21 sh -c '
set -ex
apk add --no-cache build-base cmake ninja git linux-headers pkgconfig \
openssl-dev openssl-libs-static nghttp2-dev nghttp2-static zlib-static
git config --global --add safe.directory /src
rm -rf build-rel
cmake -S . -B build-rel -GNinja -DCMAKE_BUILD_TYPE=Release \
-DAGENTTY_STANDALONE=ON -DAGENTTY_FULLY_STATIC=ON \
-DAGENTTY_AUTO_PULL_MAYA=OFF -DAGENTTY_USE_MIMALLOC=OFF \
-DAGENTTY_ARCH="$CMARCH"
cmake --build build-rel -j"$(nproc)"
strip build-rel/agentty
cp build-rel/agentty agentty-linux-x86_64
'
- name: Upload to release + stash for packaging
run: gh release upload "$TAG" agentty-linux-x86_64 --clobber
- uses: actions/upload-artifact@v4
with:
name: bin-linux-x86_64
path: agentty-linux-x86_64
if-no-files-found: error
build-linux-aarch64:
name: linux aarch64
needs: prepare
# Native ARM64 runner (GitHub-hosted, free for public repos) — NOT QEMU.
# The previous `ubuntu-latest` + docker --platform linux/arm64 path ran
# the whole C++26 compile under QEMU emulation (~10-20x slower, 1h+),
# which stalled every downstream package that pins the arm64 binary. On
# real ARM silicon this finishes in ~the same time as the x86_64 leg, so
# aarch64 is a first-class fast arch again and nothing waits on it.
runs-on: ubuntu-24.04-arm
env:
TAG: ${{ needs.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v5
- name: Init submodules (HTTPS, race-tolerant)
run: |
git config --global url."https://github.com/".insteadOf "git@github.com:"
for attempt in 1 2 3 4 5; do
if git submodule update --init --recursive --force; then break; fi
echo "submodule update failed (attempt $attempt) — full-fetching and retrying"
git submodule sync --recursive
git submodule foreach --recursive 'git fetch --prune origin || true'
sleep $((attempt * 10))
done
git submodule update --init --recursive --force
- name: Build static binary (Alpine 3.21 / musl / GCC 14)
# Host is already arm64, so a plain alpine:3.21 container builds
# natively — no --platform, no QEMU.
run: |
docker run --rm \
-e CMARCH='native' \
-v "$PWD":/src -w /src alpine:3.21 sh -c '
set -ex
apk add --no-cache build-base cmake ninja git linux-headers pkgconfig \
openssl-dev openssl-libs-static nghttp2-dev nghttp2-static zlib-static
git config --global --add safe.directory /src
rm -rf build-rel
cmake -S . -B build-rel -GNinja -DCMAKE_BUILD_TYPE=Release \
-DAGENTTY_STANDALONE=ON -DAGENTTY_FULLY_STATIC=ON \
-DAGENTTY_AUTO_PULL_MAYA=OFF -DAGENTTY_USE_MIMALLOC=OFF \
-DAGENTTY_ARCH="$CMARCH"
cmake --build build-rel -j"$(nproc)"
strip build-rel/agentty
# Guard against the -static-pie spec bug (Debian #996326): a
# broken link yields an ET_DYN that still has a PT_INTERP /
# NEEDED libc and no self-relocating PHDR, which aborts on
# Android/Bionic with "Could not find a PHDR". Write a diagnosis
# file (uploaded even on failure) and fail the job here instead
# of shipping an unrunnable aarch64 asset.
{
echo "### readelf -h"; readelf -h build-rel/agentty
echo "### program headers (INTERP/PHDR)"; readelf -l build-rel/agentty | grep -E "Type|PHDR|INTERP|LOAD|DYNAMIC"
echo "### dynamic (NEEDED)"; readelf -d build-rel/agentty 2>/dev/null | grep -E "NEEDED|FLAGS" || echo "(no dynamic section)"
} > aarch64.diag.txt 2>&1 || true
cat aarch64.diag.txt
etype=$(readelf -h build-rel/agentty | awk "/Type:/{print \$2}")
cp build-rel/agentty agentty-linux-aarch64
if [ "$etype" != "DYN" ]; then echo "WARN: not ET_DYN ($etype)"; fi
if readelf -l build-rel/agentty | grep -q INTERP; then echo "WARN: has PT_INTERP (dynamic-pie, not static-pie)"; fi
if readelf -d build-rel/agentty 2>/dev/null | grep -q NEEDED; then echo "WARN: has NEEDED (libc not static)"; fi
'
- name: Upload to release + stash for packaging
run: gh release upload "$TAG" agentty-linux-aarch64 aarch64.diag.txt --clobber
- uses: actions/upload-artifact@v4
with:
name: bin-linux-aarch64
path: agentty-linux-aarch64
if-no-files-found: error
build-linux-i686:
name: linux i686
needs: prepare
runs-on: ubuntu-latest
env:
TAG: ${{ needs.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v5
- name: Init submodules (HTTPS, race-tolerant)
run: |
git config --global url."https://github.com/".insteadOf "git@github.com:"
for attempt in 1 2 3 4 5; do
if git submodule update --init --recursive --force; then break; fi
echo "submodule update failed (attempt $attempt) — full-fetching and retrying"
git submodule sync --recursive
git submodule foreach --recursive 'git fetch --prune origin || true'
sleep $((attempt * 10))
done
git submodule update --init --recursive --force
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Build static binary (Alpine 3.21 / musl / GCC 14)
run: |
docker run --rm --platform linux/386 \
-e CMARCH='sse2' \
-v "$PWD":/src -w /src alpine:3.21 sh -c '
set -ex
apk add --no-cache build-base cmake ninja git linux-headers pkgconfig \
openssl-dev openssl-libs-static nghttp2-dev nghttp2-static zlib-static
git config --global --add safe.directory /src
rm -rf build-rel
cmake -S . -B build-rel -GNinja -DCMAKE_BUILD_TYPE=Release \
-DAGENTTY_STANDALONE=ON -DAGENTTY_FULLY_STATIC=ON \
-DAGENTTY_AUTO_PULL_MAYA=OFF -DAGENTTY_USE_MIMALLOC=OFF \
-DAGENTTY_ARCH="$CMARCH"
cmake --build build-rel -j"$(nproc)"
strip build-rel/agentty
cp build-rel/agentty agentty-linux-i686
'
- name: Upload to release
run: gh release upload "$TAG" agentty-linux-i686 --clobber
build-windows:
name: windows x86_64
needs: prepare
runs-on: windows-latest
env:
TAG: ${{ needs.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v5
- name: Init submodules (HTTPS, race-tolerant)
shell: bash
run: |
git config --global url."https://github.com/".insteadOf "git@github.com:"
for attempt in 1 2 3 4 5; do
if git submodule update --init --recursive --force; then break; fi
echo "submodule update failed (attempt $attempt) — full-fetching and retrying"
git submodule sync --recursive
git submodule foreach --recursive 'git fetch --prune origin || true'
sleep $((attempt * 10))
done
git submodule update --init --recursive --force
- name: Install deps (vcpkg static)
shell: pwsh
run: |
git -C "$env:VCPKG_INSTALLATION_ROOT" fetch --depth 1 origin master
git -C "$env:VCPKG_INSTALLATION_ROOT" reset --hard FETCH_HEAD
& "$env:VCPKG_INSTALLATION_ROOT\bootstrap-vcpkg.bat"
& "$env:VCPKG_INSTALLATION_ROOT\vcpkg.exe" install openssl nghttp2 --triplet x64-windows-static
- name: Configure + build
shell: pwsh
run: |
cmake -B build -DCMAKE_BUILD_TYPE=Release `
-DAGENTTY_STANDALONE=ON -DAGENTTY_AUTO_PULL_MAYA=OFF `
-DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" `
-DVCPKG_TARGET_TRIPLET=x64-windows-static
cmake --build build --config Release -j
- name: Stage binary
shell: pwsh
run: |
$exe = Get-ChildItem -Recurse build -Filter agentty.exe | Select-Object -First 1
Copy-Item $exe.FullName agentty-windows-x86_64.exe
- name: Install WiX toolset + UI extension
shell: pwsh
# build-msi.ps1 invokes `wix build ... -ext WixToolset.UI.wixext`,
# which needs both the `wix` tool AND the UI extension registered.
#
# Pin to WiX **v5**: `dotnet tool install wix` (unpinned) pulls v7,
# which is gated behind the Open Source Maintenance Fee EULA and
# fails every invocation with "WIX7015: You must accept the OSMF
# EULA". v5 is the last freely-usable major and reads the v4/wxs
# schema in agentty.wxs without changes. Uninstall any preexisting
# wix first so the version is deterministic regardless of what the
# runner image preinstalled.
run: |
dotnet tool uninstall --global wix 2>$null
dotnet tool install --global wix --version 5.*
$env:PATH = "$env:USERPROFILE\.dotnet\tools;$env:PATH"
"$env:USERPROFILE\.dotnet\tools" | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8
$wixVer = (wix --version).Trim().Split('+')[0]
Write-Host "wix version: $wixVer"
# Register the UI extension at the tool's exact version so the two
# never skew. `wix extension add` is idempotent.
wix extension add -g "WixToolset.UI.wixext/$wixVer"
wix extension list -g
- name: Build (and sign) MSI installer
shell: pwsh
env:
VERSION: ${{ needs.prepare.outputs.version }}
# Code signing is OPTIONAL. Two ways, pick whichever you have:
# (a) any-CA cert: base64 your .pfx into WINDOWS_CERT_BASE64 (+ password)
# (b) Azure Trusted Signing: set the TRUSTED_SIGNING_* secrets
# No secrets → a valid UNSIGNED installer is still produced.
WINDOWS_CERT_BASE64: ${{ secrets.WINDOWS_CERT_BASE64 }}
WINDOWS_CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
TRUSTED_SIGNING_ENDPOINT: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }}
TRUSTED_SIGNING_ACCOUNT: ${{ secrets.TRUSTED_SIGNING_ACCOUNT }}
TRUSTED_SIGNING_PROFILE: ${{ secrets.TRUSTED_SIGNING_PROFILE }}
run: |
# Splat a hashtable so the -Sign switch is present only when signing
# inputs exist. An empty positional splat (@()) would bind '' as a
# positional arg and fail ("positional parameter ... argument ''").
$params = @{
Version = "$env:VERSION"
Exe = 'agentty-windows-x86_64.exe'
Arch = 'x64'
}
if ($env:WINDOWS_CERT_BASE64 -or $env:TRUSTED_SIGNING_ENDPOINT) { $params.Sign = $true }
./packaging/windows/build-msi.ps1 @params
- name: Upload to release
shell: bash
run: gh release upload "$TAG" agentty-windows-x86_64.exe agentty-windows-x86_64.msi --clobber
build-macos:
name: macos ${{ matrix.arch }}
needs: prepare
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
runner: macos-15 # Apple Silicon
- arch: x86_64
runner: macos-15-intel # Intel (macos-13 retired)
runs-on: ${{ matrix.runner }}
env:
TAG: ${{ needs.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v5
- name: Init submodules (HTTPS, race-tolerant)
run: |
git config --global url."https://github.com/".insteadOf "git@github.com:"
for attempt in 1 2 3 4 5; do
if git submodule update --init --recursive --force; then break; fi
echo "submodule update failed (attempt $attempt) — full-fetching and retrying"
git submodule sync --recursive
git submodule foreach --recursive 'git fetch --prune origin || true'
sleep $((attempt * 10))
done
git submodule update --init --recursive --force
- name: Install deps (Homebrew)
# GCC, NOT AppleClang: agentty + maya require C++26 (std::expected,
# std::format) requested via CMake's cxx_std_26 compile feature, which
# AppleClang doesn't advertise (configure fails in maya). openssl@3
# ships a static .a; nghttp2 ships only the dylib, so it's built from
# source below.
run: brew install gcc openssl@3 ninja
- name: Build static nghttp2 (Homebrew ships only the dylib)
run: |
ver=1.64.0
curl -fsSL "https://github.com/nghttp2/nghttp2/releases/download/v${ver}/nghttp2-${ver}.tar.gz" | tar xz
cd "nghttp2-${ver}"
./configure --prefix="$PWD/../nghttp2-static" \
--enable-lib-only --enable-static --disable-shared CFLAGS="-O2"
make -j"$(sysctl -n hw.ncpu)"
make install
- name: Configure + build (standalone)
run: |
gcc_pfx="$(brew --prefix gcc)"
# Resolve the versioned compilers (gcc-15, g++-15 …) without pinning
# a version Homebrew may bump out from under us.
GCC="$(ls "$gcc_pfx"/bin/gcc-* | grep -E 'gcc-[0-9]+$' | sort -V | tail -1)"
GXX="$(ls "$gcc_pfx"/bin/g++-* | grep -E 'g\+\+-[0-9]+$' | sort -V | tail -1)"
# Fold GCC's runtime into the binary so it runs without Homebrew GCC
# present: -static-libstdc++/-static-libgcc cover libstdc++/libgcc,
# but libquadmath has no -static flag — link its archive by full path
# (macOS ld picks the dylib otherwise). Result: only the system
# frameworks + libSystem remain dynamic.
QUADMATH="$gcc_pfx/lib/gcc/current/libquadmath.a"
PKG_CONFIG_PATH="$PWD/nghttp2-static/lib/pkgconfig" \
CC="$GCC" CXX="$GXX" cmake -B build -GNinja -DCMAKE_BUILD_TYPE=Release \
-DAGENTTY_STANDALONE=ON -DAGENTTY_AUTO_PULL_MAYA=OFF \
-DAGENTTY_USE_MIMALLOC=OFF \
-DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" \
-DCMAKE_EXE_LINKER_FLAGS="-static-libstdc++ -static-libgcc $QUADMATH"
cmake --build build -j"$(sysctl -n hw.ncpu)"
strip build/agentty
cp build/agentty agentty-macos-${{ matrix.arch }}
- name: Verify portability (no Homebrew dylibs)
run: |
otool -L agentty-macos-${{ matrix.arch }}
if otool -L agentty-macos-${{ matrix.arch }} | grep -qE '/opt/homebrew|/usr/local'; then
echo "::error::binary links Homebrew dylibs — not portable"; exit 1
fi
- name: Upload to release
run: gh release upload "$TAG" agentty-macos-${{ matrix.arch }} --clobber
# x86_64 packages — deb (amd64) / rpm (x86_64) / Arch (x86_64). Needs only
# the x86 build, so it publishes ~5 min in without waiting on aarch64.
package-linux-x86_64:
name: packages x86_64
needs: [prepare, build-linux-x86_64]
runs-on: ubuntu-latest
env:
V: ${{ needs.prepare.outputs.version }}
TAG: ${{ needs.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v5
- uses: actions/download-artifact@v4
with:
name: bin-linux-x86_64
path: bins
- name: Tooling
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends rpm libarchive-tools zstd
- name: Build + upload .deb / .rpm / .pkg.tar.zst
run: |
mkdir -p dist
chmod +x bins/agentty-linux-*
# deb/build.sh cd's into a tmpdir, so the output dir must be absolute.
bash packaging/deb/build.sh "$V" amd64 bins/agentty-linux-x86_64 "$PWD/dist"
bash packaging/rpm/build.sh "$V" x86_64 bins/agentty-linux-x86_64 "$PWD/dist"
# Arch .pkg.tar.zst built by hand (bsdtar + zstd) — same philosophy as
# the deb builder. Avoids makepkg's fakeroot, which is unreliable in
# containers ("libfakeroot: payload not recognized").
aroot="$(mktemp -d)"
install -Dm755 bins/agentty-linux-x86_64 "$aroot/usr/bin/agentty"
asize=$(du -sb "$aroot" | cut -f1)
{
echo "pkgname = agentty-bin"
echo "pkgbase = agentty-bin"
echo "pkgver = $V-1"
echo "pkgdesc = Blazing-fast Claude in your terminal (C++26, static binary)"
echo "url = https://github.com/1ay1/agentty"
echo "builddate = $(date +%s)"
echo "packager = agentty CI <noreply@github.com>"
echo "size = $asize"
echo "arch = x86_64"
echo "license = MIT"
echo "provides = agentty"
echo "conflict = agentty"
} > "$aroot/.PKGINFO"
bsdtar -C "$aroot" -czf "$aroot/.MTREE" --format=mtree \
--options='!all,use-set,type,uid,gid,mode,time,size,md5,sha256,link' \
.PKGINFO usr
bsdtar -C "$aroot" -cf - .PKGINFO .MTREE usr \
| zstd -19 -T0 > "dist/agentty-bin-${V}-1-x86_64.pkg.tar.zst"
gh release upload "$TAG" dist/*.deb dist/*.rpm dist/*.pkg.tar.zst --clobber
# aarch64 packages — deb (arm64) / rpm (aarch64). Needs only the aarch64
# build, so it streams in when that QEMU build finishes (no Arch pkg: the
# AUR agentty-bin is x86_64-only).
package-linux-aarch64:
name: packages aarch64
needs: [prepare, build-linux-aarch64]
runs-on: ubuntu-latest
env:
V: ${{ needs.prepare.outputs.version }}
TAG: ${{ needs.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v5
- uses: actions/download-artifact@v4
with:
name: bin-linux-aarch64
path: bins
- name: Tooling
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends rpm
- name: Build + upload .deb / .rpm
run: |
mkdir -p dist
chmod +x bins/agentty-linux-*
# deb/build.sh cd's into a tmpdir, so the output dir must be absolute.
bash packaging/deb/build.sh "$V" arm64 bins/agentty-linux-aarch64 "$PWD/dist"
bash packaging/rpm/build.sh "$V" aarch64 bins/agentty-linux-aarch64 "$PWD/dist"
gh release upload "$TAG" dist/*.deb dist/*.rpm --clobber
# SHA256SUMS over whatever is on the release. Runs TWICE by design:
# * early — gated only on the fast x86_64/windows/macos legs, so the
# checksum file exists within ~5 min for everything that's landed.
# * late — a second job (checksums-final) gated on ALL builds refreshes
# it once the slow QEMU arches (aarch64/i686) finish, so the final file
# covers every asset.
# Both use `always()` + hash `*`, so a slow or failed leg never blocks the
# file for the assets that DID land. This matters because a consumer (e.g.
# an AUR PKGBUILD verifying against SHA256SUMS) 404s if the file is missing
# while the slow build drags on.
checksums:
name: checksums (early)
needs:
- prepare
- build-linux-x86_64
- build-macos
- build-windows
- package-linux-x86_64
if: always()
runs-on: ubuntu-latest
env:
TAG: ${{ needs.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v5 # gh needs a git repo context for release ops
- name: Download release assets, hash, re-upload
run: |
mkdir -p dist && cd dist
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern '*' --skip-existing || true
# Never hash the checksum file itself into itself.
rm -f SHA256SUMS
sha256sum -- * > SHA256SUMS
# --clobber alone races with checksums-final on the same asset name
# (both delete+create SHA256SUMS): the loser hits
# "HTTP 422 ReleaseAsset.name already exists". Delete first, then retry.
for attempt in 1 2 3; do
gh release delete-asset "$TAG" SHA256SUMS --yes 2>/dev/null || true
if gh release upload "$TAG" SHA256SUMS --clobber; then
exit 0
fi
echo "upload attempt $attempt failed (asset race); retrying in 5s" >&2
sleep 5
done
echo "::error::failed to upload SHA256SUMS after 3 attempts"; exit 1
# Late refresh once the slow QEMU arches land, so SHA256SUMS covers them too.
checksums-final:
name: checksums (final)
needs:
- prepare
- checksums
- build-linux-x86_64
- build-linux-aarch64
- build-linux-i686
- build-macos
- build-windows
- package-linux-x86_64
- package-linux-aarch64
if: always()
runs-on: ubuntu-latest
env:
TAG: ${{ needs.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v5
- name: Download release assets, hash, re-upload
run: |
mkdir -p dist && cd dist
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern '*' --skip-existing || true
# Never hash the checksum file itself into itself.
rm -f SHA256SUMS
sha256sum -- * > SHA256SUMS
# Delete-then-upload with retry, so a transient asset-name collision
# ("HTTP 422 ReleaseAsset.name already exists") self-heals.
for attempt in 1 2 3; do
gh release delete-asset "$TAG" SHA256SUMS --yes 2>/dev/null || true
if gh release upload "$TAG" SHA256SUMS --clobber; then
exit 0
fi
echo "upload attempt $attempt failed (asset race); retrying in 5s" >&2
sleep 5
done
echo "::error::failed to upload SHA256SUMS after 3 attempts"; exit 1
# Alpine .apk — built inside an Alpine container with abuild, attached to the
# release so users can `apk add ./agentty-*.apk` before the aports APKBUILD is
# merged upstream. x86_64 only in CI (aarch64 would need QEMU + abuild).
package-alpine:
name: package alpine (.apk)
needs: [prepare, build-linux-x86_64]
runs-on: ubuntu-latest
env:
V: ${{ needs.prepare.outputs.version }}
TAG: ${{ needs.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v5
- uses: actions/download-artifact@v4
with:
name: bin-linux-x86_64
path: bins
- name: Build .apk (Alpine 3.21 / abuild)
run: |
chmod +x bins/agentty-linux-x86_64
docker run --rm -v "$PWD":/src -w /src -e V="$V" alpine:3.21 sh -c '
set -ex
apk add --no-cache alpine-sdk abuild
adduser -D builder && addgroup builder abuild
su builder -c "abuild-keygen -a -n"
work=/home/builder/agentty; mkdir -p "$work"
# Central version: @VERSION@ -> $V. Point source at the local binary
# and drop the aarch64 line so a single-arch x86_64 build succeeds.
sed -e "s/^pkgver=@VERSION@/pkgver=$V/" \
-e "s|^\tagentty-linux-x86_64::.*|\tagentty-linux-x86_64::agentty-linux-x86_64|" \
-e "/agentty-linux-aarch64::/d" \
packaging/alpine/APKBUILD > "$work/APKBUILD"
cp bins/agentty-linux-x86_64 "$work/agentty-linux-x86_64"
chown -R builder:builder "$work"
su builder -c "cd $work && abuild checksum && abuild -r -P /home/builder/pkgs" || true
find /home/builder/pkgs -name "agentty-*.apk" -exec cp {} /src/ \; || true
'
ls -la agentty-*.apk 2>/dev/null || echo "no .apk produced (non-fatal)"
- name: Upload .apk to release
run: |
if ls agentty-*.apk >/dev/null 2>&1; then
gh release upload "$TAG" agentty-*.apk --clobber
fi
# Version-pinned downstream manifests (nix/snap/gentoo/alpine) rendered from
# the release binaries and attached to the release. Same sed pinning as
# scripts/release.sh — CMake project VERSION stays the single source of truth.
# Pins from SHA256SUMS + verifies the bytes match before attaching.
publish-manifests:
name: publish manifests (nix/snap/gentoo)
needs: [prepare, checksums-final, build-linux-x86_64, build-linux-aarch64]
if: always() && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
env:
V: ${{ needs.prepare.outputs.version }}
TAG: ${{ needs.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v5
- name: Pin + attach manifests
run: |
set -eu
mkdir -p out
# Authoritative hashes from SHA256SUMS; verify bytes still match so a
# re-run never renders a manifest that drifted from the release.
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \
--pattern 'agentty-linux-x86_64' --pattern 'agentty-linux-aarch64' \
--pattern 'SHA256SUMS' --skip-existing
sum_for() {
want=$(awk -v f="$1" '$2==f {print $1}' SHA256SUMS)
[ -n "$want" ] || { echo "::error::$1 missing from SHA256SUMS"; exit 1; }
got=$(sha256sum "$1" | awk '{print $1}')
[ "$want" = "$got" ] || { echo "::error::$1 hash drift: SHA256SUMS=$want bytes=$got"; exit 1; }
printf '%s' "$want"
}
x64=$(sum_for agentty-linux-x86_64)
arm=$(sum_for agentty-linux-aarch64)
sed -e "s/@VERSION@/$V/g" \
-e "s|@LINUX_X86_64_SHA256@|$x64|g" \
-e "s|@LINUX_AARCH64_SHA256@|$arm|g" \
packaging/nix/default.nix > out/default.nix
sed -e "s/@VERSION@/$V/g" packaging/snap/snapcraft.yaml.in > out/snapcraft.yaml
cp packaging/gentoo/agentty-9999.ebuild "out/agentty-$V.ebuild"
gh release upload "$TAG" out/default.nix out/snapcraft.yaml out/agentty-*.ebuild --clobber
# Homebrew tap: bump version + pin all four (linux/macos × arch) sha256, then
# push the formula to 1ay1/homebrew-tap. Needs TAP_TOKEN (PAT, repo scope).
# Pins from the release's SHA256SUMS (single source of truth) + verifies the
# downloaded bytes match, so a re-run can never publish a stale hash.
publish-homebrew:
name: publish to homebrew tap
needs: [prepare, checksums-final, build-linux-x86_64, build-linux-aarch64, build-macos]
if: always() && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
env:
V: ${{ needs.prepare.outputs.version }}
TAG: ${{ needs.prepare.outputs.tag }}
TAP_TOKEN: ${{ secrets.TAP_TOKEN }}
steps:
- uses: actions/checkout@v5
- name: Render + push formula
if: ${{ env.TAP_TOKEN != '' }}
run: |
set -eu
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \
--pattern 'agentty-linux-x86_64' --pattern 'agentty-linux-aarch64' \
--pattern 'agentty-macos-x86_64' --pattern 'agentty-macos-arm64' \
--pattern 'SHA256SUMS' --skip-existing
# SHA256SUMS on the release is the single source of truth. Read the
# hash from it, and verify the downloaded bytes still match — so a
# re-run can never pin a hash that has drifted from the release.
sum_for() {
want=$(awk -v f="$1" '$2==f {print $1}' SHA256SUMS)
[ -n "$want" ] || { echo "::error::$1 missing from SHA256SUMS"; exit 1; }
got=$(sha256sum "$1" | awk '{print $1}')
[ "$want" = "$got" ] || { echo "::error::$1 hash drift: SHA256SUMS=$want bytes=$got — release was re-clobbered; refusing to pin a stale hash"; exit 1; }
printf '%s' "$want"
}
sed -e "s/^ version \".*\"/ version \"$V\"/" \
-e "s|@LINUX_X86_64_SHA256@|$(sum_for agentty-linux-x86_64)|g" \
-e "s|@LINUX_AARCH64_SHA256@|$(sum_for agentty-linux-aarch64)|g" \
-e "s|@MACOS_X86_64_SHA256@|$(sum_for agentty-macos-x86_64)|g" \
-e "s|@MACOS_ARM64_SHA256@|$(sum_for agentty-macos-arm64)|g" \
packaging/homebrew/agentty.rb > agentty.rb
git clone "https://x-access-token:${TAP_TOKEN}@github.com/1ay1/homebrew-tap.git" tap
mkdir -p tap/Formula && cp agentty.rb tap/Formula/agentty.rb
cd tap
git config user.name agentty-ci; git config user.email noreply@github.com
git add Formula/agentty.rb
git commit -m "agentty $V" || { echo "no change"; exit 0; }
git push origin HEAD:master
# Scoop bucket: bump version + pin win sha256, push to 1ay1/scoop-bucket.
# Needs SCOOP_TOKEN (PAT, repo scope). Skipped when absent. Depends only on
# the windows build — fires without waiting on any linux/macos leg.
publish-scoop:
name: publish to scoop bucket
needs: [prepare, checksums-final, build-windows]
if: always() && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
env:
V: ${{ needs.prepare.outputs.version }}
TAG: ${{ needs.prepare.outputs.tag }}
SCOOP_TOKEN: ${{ secrets.SCOOP_TOKEN }}
steps:
- uses: actions/checkout@v5
- name: Render + push manifest
if: ${{ env.SCOOP_TOKEN != '' }}
run: |
set -eu
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \
--pattern 'agentty-windows-x86_64.exe' --pattern 'SHA256SUMS' --skip-existing
# Authoritative hash from SHA256SUMS; verify bytes still match so a
# re-run never pins a hash that drifted from the release.
win=$(awk '$2=="agentty-windows-x86_64.exe" {print $1}' SHA256SUMS)
[ -n "$win" ] || { echo "::error::windows .exe missing from SHA256SUMS"; exit 1; }
got=$(sha256sum agentty-windows-x86_64.exe | awk '{print $1}')
[ "$win" = "$got" ] || { echo "::error::windows .exe hash drift: SHA256SUMS=$win bytes=$got"; exit 1; }
sed -e "s/\"version\": \".*\"/\"version\": \"$V\"/" \
-e "s|/download/v[0-9.]*/|/download/v$V/|g" \
-e "s|@WINDOWS_X86_64_SHA256@|$win|g" \
packaging/scoop/agentty.json > agentty.json
git clone "https://x-access-token:${SCOOP_TOKEN}@github.com/1ay1/scoop-bucket.git" bucket
mkdir -p bucket/bucket && cp agentty.json bucket/bucket/agentty.json
cd bucket
git config user.name agentty-ci; git config user.email noreply@github.com
git add bucket/agentty.json
git commit -m "agentty $V" || { echo "no change"; exit 0; }
git push origin HEAD:master
# AUR: push the version-pinned PKGBUILD (agentty-bin). Needs AUR_SSH_KEY, an
# SSH private key registered with the AUR account. Skipped when absent.
# Pins from SHA256SUMS + verifies the bytes match before pushing.
publish-aur:
name: publish to AUR
needs: [prepare, checksums-final, build-linux-x86_64, build-linux-aarch64]
if: always() && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
env:
V: ${{ needs.prepare.outputs.version }}
TAG: ${{ needs.prepare.outputs.tag }}
AUR_SSH_KEY: ${{ secrets.AUR_SSH_KEY }}
steps:
- uses: actions/checkout@v5
- name: Render + push PKGBUILD
if: ${{ env.AUR_SSH_KEY != '' }}
run: |
set -eu
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \
--pattern 'agentty-linux-x86_64' --pattern 'agentty-linux-aarch64' \
--pattern 'SHA256SUMS' --skip-existing
# Authoritative hashes from SHA256SUMS; verify bytes still match.
sum_for() {
want=$(awk -v f="$1" '$2==f {print $1}' SHA256SUMS)
[ -n "$want" ] || { echo "::error::$1 missing from SHA256SUMS"; exit 1; }
got=$(sha256sum "$1" | awk '{print $1}')
[ "$want" = "$got" ] || { echo "::error::$1 hash drift: SHA256SUMS=$want bytes=$got"; exit 1; }
printf '%s' "$want"
}
x64=$(sum_for agentty-linux-x86_64)
arm=$(sum_for agentty-linux-aarch64)
sed -e "s/^pkgver=.*/pkgver=$V/" \
-e "s/sha256sums_x86_64=.*/sha256sums_x86_64=('$x64')/" \
-e "s/sha256sums_aarch64=.*/sha256sums_aarch64=('$arm')/" \
packaging/arch/PKGBUILD > PKGBUILD
mkdir -p ~/.ssh
printf '%s\n' "$AUR_SSH_KEY" > ~/.ssh/aur; chmod 600 ~/.ssh/aur
ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null
export GIT_SSH_COMMAND="ssh -i ~/.ssh/aur"
git clone ssh://aur@aur.archlinux.org/agentty-bin.git aur
cp PKGBUILD aur/PKGBUILD
cd aur
# .SRCINFO must mirror the PKGBUILD; generate it via makepkg if the
# runner has it, else hand-write the minimal fields the AUR requires.
if command -v makepkg >/dev/null 2>&1; then
makepkg --printsrcinfo > .SRCINFO
else
printf 'pkgbase = agentty-bin\n\tpkgdesc = Blazing-fast Claude in your terminal\n\tpkgver = %s\n\tpkgrel = 1\n\turl = https://github.com/1ay1/agentty\n\tarch = x86_64\n\tarch = aarch64\n\tlicense = MIT\n\tprovides = agentty\n\tconflicts = agentty\n\tsource_x86_64 = agentty-%s-x86_64::https://github.com/1ay1/agentty/releases/download/v%s/agentty-linux-x86_64\n\tsha256sums_x86_64 = %s\n\tsource_aarch64 = agentty-%s-aarch64::https://github.com/1ay1/agentty/releases/download/v%s/agentty-linux-aarch64\n\tsha256sums_aarch64 = %s\n\npkgname = agentty-bin\n' \
"$V" "$V" "$V" "$x64" "$V" "$V" "$arm" > .SRCINFO
fi
git config user.name agentty-ci; git config user.email noreply@github.com
git add PKGBUILD .SRCINFO
git commit -m "agentty $V" || { echo "no change"; exit 0; }
git push origin HEAD:master
# Submit the Windows .msi to the winget community repo. winget is the
# zero-cost, no-SmartScreen-warning install path: `winget install agentty`.
#
# Fully hands-off across BOTH cases:
# * package already in winget-pkgs -> vedantmgoyal9/winget-releaser opens
# the version-bump PR (update path).
# * package NOT yet in winget-pkgs -> that action refuses ("add at least
# one version first"), so we fall back to a first-submission step that
# renders packaging/winget/*.yaml from the CMake version + the built
# .msi's real SHA256/ProductCode and opens the initial PR via the API.
# Either way the submitted version is the central CMake project VERSION.
# Requires WINGET_TOKEN (classic PAT, public_repo scope); skipped if absent.
publish-winget:
name: publish to winget
needs: [prepare, build-windows]
if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-latest
env:
V: ${{ needs.prepare.outputs.version }}
TAG: ${{ needs.prepare.outputs.tag }}
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}
PKGID: agentty.agentty
steps:
- uses: actions/checkout@v5
# Does agentty.agentty already have a manifest folder upstream? Drives
# which of the two paths below actually runs.
- name: Check if package exists in winget-pkgs
id: exists
if: ${{ env.WINGET_TOKEN != '' }}
run: |
if gh api repos/microsoft/winget-pkgs/contents/manifests/a/agentty/agentty >/dev/null 2>&1; then
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
fi
env:
GH_TOKEN: ${{ secrets.WINGET_TOKEN }}
# UPDATE path — package already exists upstream.
- name: Update manifest (winget-releaser)
if: ${{ env.WINGET_TOKEN != '' && steps.exists.outputs.present == 'true' }}
# Non-fatal: winget is publish-once-per-version. A manual re-dispatch
# for an already-submitted version makes komac error on the
# duplicate; that's a no-op, not a release-breaking failure. The
# tagged-release path submits each version exactly once.
continue-on-error: true
uses: vedantmgoyal9/winget-releaser@main
with:
identifier: agentty.agentty
version: ${{ needs.prepare.outputs.version }}
# Explicit tag: the action's default is
# `github.event.release.tag_name || github.ref_name`, which on a
# workflow_dispatch from master resolves to "master" — komac then
# queries releases/tags/master (404), gets no asset URLs, and
# fails. prepare.outputs.tag is the real vX.Y.Z on both the
# tagged-release and manual-dispatch paths.
release-tag: ${{ needs.prepare.outputs.tag }}
installers-regex: 'agentty-windows-x86_64\.msi$'
token: ${{ secrets.WINGET_TOKEN }}
# FIRST-SUBMISSION path — package not yet upstream. Render the templates
# from the central version + the exact built .msi and open the initial PR.
- name: First submission (render + PR via API)
if: ${{ env.WINGET_TOKEN != '' && steps.exists.outputs.present == 'false' }}
env:
GH_TOKEN: ${{ secrets.WINGET_TOKEN }}
run: |
set -eu
sudo apt-get update && sudo apt-get install -y --no-install-recommends msitools >/dev/null
# 1. Fetch the built .msi; compute SHA256 + ProductCode from it.
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \
--pattern 'agentty-windows-x86_64.msi' --output a.msi
MSI_SHA=$(sha256sum a.msi | awk '{print toupper($1)}')
# msiinfo dumps the Property table; ProductCode is a GUID in braces.
PC=$(msiinfo export a.msi Property 2>/dev/null \
| awk -F'\t' '$1=="ProductCode"{print $2}')
[ -n "$PC" ] || { echo "::error::could not read ProductCode from MSI"; exit 1; }
echo "msi sha=$MSI_SHA productcode=$PC"
# 2. Render the three templates. Keep the schema line (needed by
# winget) but drop the template-only notes, which we mark with a
# leading '#!' so they're trivially strippable.
mkdir -p out
for f in agentty.agentty.installer.yaml agentty.agentty.locale.en-US.yaml agentty.agentty.yaml; do
sed -e "s/@VERSION@/$V/g" \
-e "s|@MSI_SHA256@|$MSI_SHA|g" \
-e "s|@MSI_PRODUCT_CODE@|$PC|g" \
"packaging/winget/$f" \
| grep -v '^#!' > "out/$f"
done
# 3. Fork winget-pkgs (default branch only; no-op if it exists).
gh api repos/microsoft/winget-pkgs/forks -X POST -f default_branch_only=true >/dev/null || true
me=$(gh api user -q .login)
# Wait for the fork to become queryable.
for i in $(seq 1 30); do
if gh api "repos/$me/winget-pkgs/git/ref/heads/master" >/dev/null 2>&1; then break; fi
sleep 4
done
# 4. Build a commit (blobs -> tree -> commit -> branch) via the API so
# we never clone the multi-GB winget-pkgs repo.
branch="agentty-agentty-$V"
dir="manifests/a/agentty/agentty/$V"
base_sha=$(gh api "repos/$me/winget-pkgs/git/ref/heads/master" -q '.object.sha')
base_tree=$(gh api "repos/$me/winget-pkgs/git/commits/$base_sha" -q '.tree.sha')
items='[]'
for f in agentty.agentty.installer.yaml agentty.agentty.locale.en-US.yaml agentty.agentty.yaml; do
b64=$(base64 -w0 "out/$f")
blob=$(jq -n --arg c "$b64" '{content:$c,encoding:"base64"}' \
| gh api "repos/$me/winget-pkgs/git/blobs" --input - -q '.sha')
items=$(echo "$items" | jq --arg p "$dir/$f" --arg s "$blob" \
'. + [{path:$p,mode:"100644",type:"blob",sha:$s}]')
done
new_tree=$(jq -n --arg bt "$base_tree" --argjson t "$items" '{base_tree:$bt,tree:$t}' \
| gh api "repos/$me/winget-pkgs/git/trees" --input - -q '.sha')
new_commit=$(jq -n --arg m "New version: agentty.agentty version $V" --arg t "$new_tree" --arg p "$base_sha" \
'{message:$m,tree:$t,parents:[$p]}' \
| gh api "repos/$me/winget-pkgs/git/commits" --input - -q '.sha')
# Recreate the branch idempotently.
gh api -X DELETE "repos/$me/winget-pkgs/git/refs/heads/$branch" >/dev/null 2>&1 || true
jq -n --arg r "refs/heads/$branch" --arg s "$new_commit" '{ref:$r,sha:$s}' \
| gh api "repos/$me/winget-pkgs/git/refs" --input - >/dev/null
# 5. Open the PR against microsoft/winget-pkgs.
body="First-time submission of agentty.agentty ($V). agentty is a native C++26 terminal coding agent: a single static binary, sandboxed by default, one-command SSH air-gap. https://github.com/1ay1/agentty"
jq -n --arg t "New version: agentty.agentty version $V" --arg h "$me:$branch" --arg b "$body" \
'{title:$t,head:$h,base:"master",body:$b}' \
| gh api repos/microsoft/winget-pkgs/pulls --input - -q '.html_url'
# Fedora COPR: submit a source build so `dnf/yum/zypper install agentty` works
# from the project's own COPR repo. This is the cheapest RPM channel — no
# distro-review wait, builds for every enabled Fedora/EL/openSUSE chroot.
#
# One-time setup (you):
# 1. Create the project at https://copr.fedorainfracloud.org (name it
# `agentty`; enable the fedora-*/epel-*/opensuse-* chroots you want).
# 2. Copr → API → copy the whole `[copr-cli]` token block.
# 3. gh secret set COPR_CONFIG --repo 1ay1/agentty < the-token-file
# 4. gh secret set COPR_PROJECT --repo 1ay1/agentty --body '<user>/agentty'
# Users then: dnf copr enable <user>/agentty && dnf install agentty
#
# We hand COPR the version-pinned .spec + the prebuilt binary as an SRPM so
# COPR's builders don't recompile C++26 (matching every other channel). The
# version is the central CMake project VERSION (via prepare.outputs.version).
publish-copr:
name: publish to Fedora COPR
needs: [prepare, build-linux-x86_64]
if: always() && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
container: fedora:41
env:
V: ${{ needs.prepare.outputs.version }}
TAG: ${{ needs.prepare.outputs.tag }}
COPR_CONFIG: ${{ secrets.COPR_CONFIG }}
COPR_PROJECT: ${{ secrets.COPR_PROJECT }}
steps:
- uses: actions/checkout@v5
- name: Build SRPM + submit to COPR
if: ${{ env.COPR_CONFIG != '' && env.COPR_PROJECT != '' }}
run: |
set -eu
dnf install -y --setopt=install_weak_deps=False \
rpm-build rpmdevtools copr-cli gh tar >/dev/null
# Central version -> .spec. The spec packages the prebuilt static
# binary fetched from the release (Source0), so no compile on COPR.
rpmdev-setuptree
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \
--pattern 'agentty-linux-x86_64' --output ~/rpmbuild/SOURCES/agentty-linux-x86_64
sed -e "s/@VERSION@/$V/g" \
-e "s/@RPM_ARCH@/x86_64/g" \
-e "s/@DATE@/$(LC_ALL=C date '+%a %b %d %Y')/g" \
packaging/rpm/agentty.spec.in > ~/rpmbuild/SPECS/agentty.spec
# Build a SOURCE rpm only (-bs) — COPR rebuilds it in each chroot.
rpmbuild -bs ~/rpmbuild/SPECS/agentty.spec
srpm=$(ls ~/rpmbuild/SRPMS/agentty-*.src.rpm | head -1)
# copr-cli reads ~/.config/copr for auth.
mkdir -p ~/.config
printf '%s\n' "$COPR_CONFIG" > ~/.config/copr
chmod 600 ~/.config/copr
copr-cli build "$COPR_PROJECT" "$srpm"