Repository navigation
1132 lines (1013 loc) · 45.6 KB
/
Copy pathrelease.yml
File metadata and controls
1132 lines (1013 loc) · 45.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
993
994
995
996
997
998
999
1000
name: Release
on:
push:
tags:
- "v*"
# Branch validation runs the gate and the build fleet but leaves every
# publishing job skipped. Tag pushes retain the production release DAG.
workflow_dispatch:
concurrency:
# Include SHA so a retag at a different commit gets its own concurrency
# group and won't get blocked behind a stuck zombie queued run.
group: release-${{ github.ref }}-${{ github.sha }}
cancel-in-progress: false
permissions:
contents: write
id-token: write
env:
CARGO_TERM_COLOR: always
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
SCCACHE_IDLE_TIMEOUT: "0"
# Tags restore the main baseline without creating tag-private compiler caches.
SCCACHE_GHA_RW_MODE: ${{ github.ref == 'refs/heads/main' && 'READ_WRITE' || 'READ_ONLY' }}
jobs:
# Main CI remains a prerequisite on the exact tagged commit. Its stage
# binaries keep landing fast; the release-only E2E lanes below test shipped
# fat-LTO binaries before any publication. There is no CI-gate bypass.
gate:
name: CI gate (tests.yml green on this commit)
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Fetch main
run: git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
- name: Require a green tests.yml run for this commit
env:
GH_TOKEN: ${{ github.token }}
run: scripts/release-ci-gate.sh "$GITHUB_SHA"
# Every published JS package must build before anything is published.
# tests.yml builds the packages a test needs but never builds all of them,
# so this is not a rerun.
build-js-packages:
name: Build JS packages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-node@v5
with:
node-version: "24"
- name: Install workspace dependencies
run: bun install --frozen-lockfile
# Workspace consumers import the bridge's dist/, so it builds first.
- name: Build all JS packages
run: |
bun run --cwd packages/aft-bridge build
bun run --filter '*' build
publish-crates:
name: Publish to crates.io
if: github.event_name != 'workflow_dispatch'
runs-on: ubuntu-latest
needs:
- gate
- build-js-packages
- build-darwin-arm64
- build-darwin-x64
- build-linux-arm64
- build-linux-x64
- build-win32-arm64
- build-win32-x64
- e2e-darwin-arm64
- e2e-win32-x64
- e2e-linux-x64
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: dtolnay/rust-toolchain@stable
# Sync Cargo.toml version to match the tag. Without this, `cargo publish`
# tries to publish whatever version is in HEAD's Cargo.toml — which is
# last-released-version on tag pushes (the tag does NOT carry a separate
# version commit; release.sh commits the version bump locally, but if
# someone retags HEAD without running release.sh, Cargo.toml stays
# stale). Mirrors the same step in publish-npm-platforms.
- name: Sync versions from tag
run: node scripts/version-sync.mjs --from-tag
- name: Publish to crates.io
# Allow re-runs of the same tag: treat "already exists" as success
# ONLY if the version we're publishing matches the tag. Otherwise the
# fallback silently masks the version-mismatch bug (we try to publish
# vN, it already exists, we say success even though we wanted vN+1).
#
# `agent-file-tools` depends on `aft-tokenizer` by path; cargo refuses
# to publish a crate whose path-only dep has no `version =`. Both
# crates therefore declare versions (synced by version-sync.mjs) and
# we publish `aft-tokenizer` first so `agent-file-tools` can resolve
# the published version on crates.io. The `--no-verify` flag on the
# tokenizer publish skips the build step (saves ~30s for a leaf crate
# with no consumers in workspace until `agent-file-tools` resolves it
# from crates.io after publish settles).
run: |
TAG_VERSION="${GITHUB_REF_NAME#v}"
CARGO_VERSION=$(grep '^version' crates/aft/Cargo.toml | head -1 | sed -E 's/version = "([^"]+)"/\1/')
if [ "$TAG_VERSION" != "$CARGO_VERSION" ]; then
echo "::error::Tag $GITHUB_REF_NAME wants $TAG_VERSION but Cargo.toml has $CARGO_VERSION after version-sync. version-sync.mjs broken?"
exit 1
fi
# CRITICAL: use `true` (not `exit 0`) in the success branch of the
# || fallback. `exit 0` in `||` terminates the WHOLE script, so the
# second `cargo publish` is skipped silently when the first crate
# is already published — which is exactly the retag-recovery case
# this fallback exists for. (Repro: v0.28.1 retag — aft-tokenizer
# already existed, agent-file-tools never published.)
cargo publish --package aft-tokenizer \
|| { ec=$?; cargo publish --package aft-tokenizer --dry-run 2>&1 | grep -q "already exists" && true || exit $ec; }
# Wait briefly for the new tokenizer version to become visible on
# crates.io's sparse index before publishing `agent-file-tools` —
# otherwise the verify step in the second publish can race against
# index propagation and fail with "no matching package found".
sleep 30
cargo publish --package agent-file-tools \
|| { ec=$?; cargo publish --package agent-file-tools --dry-run 2>&1 | grep -q "already exists" && true || exit $ec; }
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
# ---------------------------------------------------------------------------
# Builds start alongside the main-CI gate. macOS ARM64, Windows x64 and
# Linux x64 hand their shipped binaries to release-only E2E, then publication
# verifies those same bytes. The three cross-built targets retain build-only
# coverage. No publication can start until all three E2E jobs succeed.
# Shipped-profile builds get 120 minutes: cold fat LTO already exceeds the
# 45-minute landing budget on macOS; rare releases can afford that headroom.
# ---------------------------------------------------------------------------
build-darwin-arm64:
name: Build macOS ARM64
runs-on: macos-26
timeout-minutes: 120
outputs:
rust_version: ${{ steps.bundle.outputs.rust_version }}
permissions:
contents: read
actions: read
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: sccache
CARGO_INCREMENTAL: "0"
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
# cache-bin: false keeps a cached ~/.cargo/bin from overwriting the fresh
# cargo shim, as in build-darwin-x64.
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: release-darwin-arm64
cache-bin: false
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Sync versions from tag
if: startsWith(github.ref, 'refs/tags/v')
run: node scripts/version-sync.mjs --from-tag
- name: Release library codegen (shipped profile)
run: cargo build --release --lib -p agent-file-tools --timings
- name: Build
run: cargo build --release --bin aft -p agent-file-tools --timings
# Strip BEFORE signing, never after: v0.51.1 shipped a SIGKILL-on-load
# arm64 binary (#238) because strip ran after signing, and strip's
# automatic re-sign produced a corrupt signature.
- name: Strip binary
run: strip target/release/aft
# Ad-hoc sign, hard-verify, and execute once. --verify --strict fails on
# an unsigned or corrupt-signature binary; the --version run proves the
# exact bytes being published start on macOS.
- name: Sign, verify, and smoke-test binary
run: |
codesign -f -s - target/release/aft
codesign --verify --strict target/release/aft
target/release/aft --version
# Package the split debug info (dSYM) as a sibling release asset, so a
# stripped binary's samples symbolicate. `ditto` dereferences the
# `aft.dSYM` symlink cargo emits before zipping, so the asset is
# self-contained.
- name: Package dSYM
run: |
mkdir -p release-dsym
ditto target/release/aft.dSYM release-dsym/aft.dSYM
ditto -c -k --keepParent release-dsym/aft.dSYM release-dsym/aft-darwin-arm64-dsym.zip
- name: Bundle shipped macOS binary and dSYM
id: bundle
run: python3 scripts/native-release-artifact.py pack-binary --archive "$RUNNER_TEMP/release.tar" --dsym release-dsym/aft-darwin-arm64-dsym.zip
- uses: actions/upload-artifact@v4
with:
name: shipped-darwin-arm64-${{ github.sha }}
path: ${{ runner.temp }}/release.tar
compression-level: 1
if-no-files-found: error
build-darwin-x64:
name: Build macOS x64
runs-on: macos-26
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: sccache
CARGO_INCREMENTAL: "0"
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-apple-darwin
- uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: release-darwin-x64
cache-bin: false
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Sync versions from tag
if: startsWith(github.ref, 'refs/tags/v')
run: node scripts/version-sync.mjs --from-tag
- name: Build
run: cargo build --release --target x86_64-apple-darwin
- name: Strip binary
run: strip target/x86_64-apple-darwin/release/aft
# Sign after the LAST binary mutation and hard-verify: v0.51.1 published
# this asset with no signature at all (#238 class - cross-compiled x64
# gets no linker signature and nothing signed it). --verify --strict
# fails the job on unsigned or corrupt-signature binaries.
- name: Sign and verify binary
run: |
codesign -f -s - target/x86_64-apple-darwin/release/aft
codesign --verify --strict target/x86_64-apple-darwin/release/aft
# Package the split debug info (dSYM) as a sibling release asset. The
# shipped binary is stripped (no symtab), so `sample`/`atos` on a deployed
# binary would otherwise return nameless offsets. `ditto` dereferences the
# `aft.dSYM` symlink cargo emits (it points at a hash-named bundle under
# deps/) and copies it to a canonical `aft.dSYM` before zipping, so the
# release asset is self-contained and keyed by the binary's UUID.
- name: Package dSYM
run: |
ditto target/x86_64-apple-darwin/release/aft.dSYM release-dsym/aft.dSYM
ditto -c -k --keepParent release-dsym/aft.dSYM release-dsym/aft-darwin-x64-dsym.zip
- uses: actions/upload-artifact@v4
with:
name: darwin-x64
path: target/x86_64-apple-darwin/release/aft
if-no-files-found: error
- uses: actions/upload-artifact@v4
with:
name: darwin-x64-dsym
path: release-dsym/aft-darwin-x64-dsym.zip
if-no-files-found: error
build-linux-arm64:
name: Build Linux ARM64
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-unknown-linux-gnu
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: release-linux-arm64
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Sync versions from tag
if: startsWith(github.ref, 'refs/tags/v')
run: node scripts/version-sync.mjs --from-tag
# Prebuilt binary via binstall — `cargo install cross` compiled it from
# source for 2-3 minutes on every release.
- name: Install cross
uses: taiki-e/install-action@ab6895351a88b908e96a4697860d4ffea5e57dbe # v2.87.27
with:
tool: cross@0.2.5
# Use gnu target (not musl) so dlopen works for ONNX Runtime loading.
# musl produces static binaries where dlopen is a stub that always fails.
- name: Build
run: cross build --release --target aarch64-unknown-linux-gnu
- uses: actions/upload-artifact@v4
with:
name: linux-arm64
path: target/aarch64-unknown-linux-gnu/release/aft
if-no-files-found: error
build-linux-x64:
name: Build Linux x64
runs-on: ubuntu-latest
timeout-minutes: 120
outputs:
rust_version: ${{ steps.bundle.outputs.rust_version }}
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: release-linux-x64
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Sync versions from tag
if: startsWith(github.ref, 'refs/tags/v')
run: node scripts/version-sync.mjs --from-tag
- name: Install cross
uses: taiki-e/install-action@ab6895351a88b908e96a4697860d4ffea5e57dbe # v2.87.27
with:
tool: cross@0.2.5
# Use gnu target (not musl) so dlopen works for ONNX Runtime loading.
# musl produces static binaries where dlopen is a stub that always fails.
- name: Build
run: cross build --release --target x86_64-unknown-linux-gnu
- name: Bundle shipped Linux binary
id: bundle
run: python3 scripts/native-release-artifact.py pack-binary --target x86_64-unknown-linux-gnu --archive "$RUNNER_TEMP/release.tar"
- uses: actions/upload-artifact@v4
with:
name: shipped-linux-x64-${{ github.sha }}
path: ${{ runner.temp }}/release.tar
compression-level: 1
if-no-files-found: error
build-win32-arm64:
name: Build Windows ARM64
# v0.28: native ARM64 binary. Host is windows-latest (x64) which
# cross-compiles to aarch64-pc-windows-msvc via the MSVC ARM64 cross
# toolchain bundled in the Visual Studio Build Tools on the runner.
runs-on: windows-latest
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: sccache
CARGO_INCREMENTAL: "0"
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-pc-windows-msvc
- uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: release-win32-arm64
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Sync versions from tag
if: startsWith(github.ref, 'refs/tags/v')
run: node scripts/version-sync.mjs --from-tag
shell: pwsh
- name: Build
run: cargo build --release --target aarch64-pc-windows-msvc
- uses: actions/upload-artifact@v4
with:
name: win32-arm64
path: target/aarch64-pc-windows-msvc/release/aft.exe
if-no-files-found: error
build-win32-x64:
name: Build Windows x64
runs-on: windows-2022
timeout-minutes: 120
outputs:
rust_version: ${{ steps.bundle.outputs.rust_version }}
permissions:
contents: read
actions: read
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: sccache
CARGO_INCREMENTAL: "0"
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: release-win32-x64
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Sync versions from tag
if: startsWith(github.ref, 'refs/tags/v')
run: node scripts/version-sync.mjs --from-tag
shell: pwsh
- name: Release library codegen (shipped profile)
run: cargo build --release --lib --target x86_64-pc-windows-msvc -p agent-file-tools --timings
shell: pwsh
- name: Build
run: cargo build --release --bin aft --target x86_64-pc-windows-msvc -p agent-file-tools --timings
shell: pwsh
# Execute the exact bytes being published once on Windows.
- name: Smoke-test binary
run: target/x86_64-pc-windows-msvc/release/aft.exe --version
shell: pwsh
- name: Bundle shipped Windows binary
id: bundle
run: python scripts/native-release-artifact.py pack-binary --target x86_64-pc-windows-msvc --archive "$env:RUNNER_TEMP/release.tar"
shell: pwsh
- uses: actions/upload-artifact@v4
with:
name: shipped-win32-x64-${{ github.sha }}
path: ${{ runner.temp }}/release.tar
compression-level: 1
if-no-files-found: error
# E2E consumers never compile or mutate release binaries. They verify the
# producer's checksum before and after testing and upload the original bundle
# only on success. Publication consumes these tested bundles, not build output.
e2e-darwin-arm64:
name: Shipped E2E (macOS ARM64)
needs: build-darwin-arm64
runs-on: macos-26
timeout-minutes: 45
env:
CARGO_INCREMENTAL: "0"
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ needs.build-darwin-arm64.outputs.rust_version }}
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Sync versions from tag
if: startsWith(github.ref, 'refs/tags/v')
run: node scripts/version-sync.mjs --from-tag
- uses: actions/download-artifact@v4
with:
name: shipped-darwin-arm64-${{ github.sha }}
path: ${{ runner.temp }}/shipped-darwin-arm64
- name: Verify shipped binary provenance
run: python3 scripts/native-release-artifact.py unpack-binary --archive "$RUNNER_TEMP/shipped-darwin-arm64/release.tar"
- name: Verify signature and warm tested binary
run: |
codesign --verify --strict target/release/aft
target/release/aft --version
- name: Install dependencies and build local plugin
run: |
bun install --frozen-lockfile
bun run --cwd packages/aft-bridge build
bun run --cwd packages/opencode-plugin build
- name: Run shipped E2E suite
run: bash tests/macos-e2e/run.sh
env:
AFT_BINARY_PATH: ${{ github.workspace }}/target/release/aft
AFT_PLUGIN_DIST: ${{ github.workspace }}/packages/opencode-plugin/dist
- name: Verify tested bytes are unchanged
run: python3 scripts/native-release-artifact.py verify-binary --archive "$RUNNER_TEMP/shipped-darwin-arm64/release.tar"
- uses: actions/upload-artifact@v4
with:
name: tested-darwin-arm64-${{ github.sha }}
path: ${{ runner.temp }}/shipped-darwin-arm64/release.tar
compression-level: 1
if-no-files-found: error
- name: Upload macOS plugin log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: shipped-e2e-macos-log-${{ github.run_attempt }}
path: ${{ runner.temp }}/aft-e2e-data/cortexkit/aft/logs/aft-plugin.log
if-no-files-found: warn
e2e-win32-x64:
name: Shipped E2E (Windows x64)
needs: build-win32-x64
runs-on: windows-2022
timeout-minutes: 45
env:
CARGO_INCREMENTAL: "0"
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ needs.build-win32-x64.outputs.rust_version }}
targets: x86_64-pc-windows-msvc
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Sync versions from tag
if: startsWith(github.ref, 'refs/tags/v')
run: node scripts/version-sync.mjs --from-tag
- uses: actions/download-artifact@v4
with:
name: shipped-win32-x64-${{ github.sha }}
path: ${{ runner.temp }}/shipped-win32-x64
- name: Verify shipped binary provenance
run: python scripts/native-release-artifact.py unpack-binary --target x86_64-pc-windows-msvc --archive "$env:RUNNER_TEMP/shipped-win32-x64/release.tar"
shell: pwsh
- name: Install dependencies and build local plugin
run: |
bun install --frozen-lockfile
bun run --cwd packages/aft-bridge build
bun run --cwd packages/opencode-plugin build
shell: pwsh
- name: Run shipped E2E suite
run: pwsh -NoProfile -ExecutionPolicy Bypass -File tests/windows-e2e/run.ps1
shell: pwsh
env:
AFT_BINARY_PATH: ${{ github.workspace }}\target\x86_64-pc-windows-msvc\release\aft.exe
AFT_PLUGIN_DIST: ${{ github.workspace }}\packages\opencode-plugin\dist
- name: Verify tested bytes are unchanged
run: python scripts/native-release-artifact.py verify-binary --target x86_64-pc-windows-msvc --archive "$env:RUNNER_TEMP/shipped-win32-x64/release.tar"
shell: pwsh
- uses: actions/upload-artifact@v4
with:
name: tested-win32-x64-${{ github.sha }}
path: ${{ runner.temp }}/shipped-win32-x64/release.tar
compression-level: 1
if-no-files-found: error
- name: Collect Windows plugin log on failure
if: failure()
shell: pwsh
run: |
$source = Join-Path $env:LOCALAPPDATA "cortexkit\aft\logs\aft-plugin.log"
if (Test-Path $source) {
Copy-Item $source "$env:RUNNER_TEMP/aft-plugin.log" -Force
}
- name: Upload Windows plugin log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: shipped-e2e-windows-log-${{ github.run_attempt }}
path: ${{ runner.temp }}/aft-plugin.log
if-no-files-found: warn
e2e-linux-x64:
name: Shipped E2E (Linux x64 Docker)
needs: build-linux-x64
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ needs.build-linux-x64.outputs.rust_version }}
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Sync versions from tag
if: startsWith(github.ref, 'refs/tags/v')
run: node scripts/version-sync.mjs --from-tag
- uses: actions/download-artifact@v4
with:
name: shipped-linux-x64-${{ github.sha }}
path: ${{ runner.temp }}/shipped-linux-x64
- name: Verify shipped binary provenance
run: python3 scripts/native-release-artifact.py unpack-binary --target x86_64-unknown-linux-gnu --archive "$RUNNER_TEMP/shipped-linux-x64/release.tar"
- name: Install dependencies and build local plugin
run: |
bun install --frozen-lockfile
bun run --cwd packages/aft-bridge build
bun run --cwd packages/opencode-plugin build
mkdir -p tests/docker/fixtures/npm-packs
npm pack ./packages/aft-bridge --pack-destination tests/docker/fixtures/npm-packs
npm pack ./packages/opencode-plugin --pack-destination tests/docker/fixtures/npm-packs
- name: Run shipped E2E suite
env:
AFT_BINARY_PATH: ${{ github.workspace }}/target/x86_64-unknown-linux-gnu/release/aft
run: |
cp "$AFT_BINARY_PATH" tests/docker/fixtures/aft-linux-x64
chmod +x tests/docker/fixtures/aft-linux-x64
docker build --platform linux/amd64 -t aft-shipped-e2e -f tests/docker/Dockerfile.linux-x64 .
container=$(docker create aft-shipped-e2e)
version=$(node -p "require('./packages/opencode-plugin/package.json').version")
docker cp "$container:/root/.cache/aft/bin/v$version/aft" "$RUNNER_TEMP/image-aft"
docker rm "$container"
python3 scripts/native-release-artifact.py verify-binary --target x86_64-unknown-linux-gnu --archive "$RUNNER_TEMP/shipped-linux-x64/release.tar" --binary "$RUNNER_TEMP/image-aft"
mkdir -p e2e-artifacts/linux
docker run --rm --platform linux/amd64 --volume "$PWD/e2e-artifacts/linux:/root/.local/share/cortexkit/aft/logs" aft-shipped-e2e
- name: Verify tested bytes are unchanged
run: python3 scripts/native-release-artifact.py verify-binary --target x86_64-unknown-linux-gnu --archive "$RUNNER_TEMP/shipped-linux-x64/release.tar"
- uses: actions/upload-artifact@v4
with:
name: tested-linux-x64-${{ github.sha }}
path: ${{ runner.temp }}/shipped-linux-x64/release.tar
compression-level: 1
if-no-files-found: error
- name: Upload Linux plugin log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: shipped-e2e-linux-log-${{ github.run_attempt }}
path: e2e-artifacts/linux/aft-plugin.log
if-no-files-found: warn
# npm publish runs AFTER github-release so that checksums.sha256 is already
# available on the GitHub release page when ensureBinary() runs during
# @cortexkit/aft-opencode@latest install.
publish-npm:
name: Publish to npm
if: github.event_name != 'workflow_dispatch'
runs-on: ubuntu-latest
needs:
- gate
- build-darwin-arm64
- build-darwin-x64
- build-linux-arm64
- build-linux-x64
- build-win32-arm64
- build-win32-x64
- e2e-darwin-arm64
- e2e-win32-x64
- e2e-linux-x64
- github-release
steps:
- uses: actions/checkout@v5
with:
# The v0.49 release gate proves the surface-manifest source commit is
# an ancestor of the candidate; a shallow (depth-1) clone makes
# merge-base fail for every historical commit and the gate
# fail-closed on a healthy release.
fetch-depth: 0
- uses: actions/setup-node@v5
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Ensure latest npm (for trusted publishing)
run: npm install -g npm@latest
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Download darwin-arm64 binary
uses: actions/download-artifact@v4
with:
name: tested-darwin-arm64-${{ github.sha }}
path: ${{ runner.temp }}/tested-darwin-arm64
- name: Download darwin-x64 binary
uses: actions/download-artifact@v4
with:
name: darwin-x64
path: packages/npm/darwin-x64/bin
- name: Download linux-arm64 binary
uses: actions/download-artifact@v4
with:
name: linux-arm64
path: packages/npm/linux-arm64/bin
- name: Download linux-x64 binary
uses: actions/download-artifact@v4
with:
name: tested-linux-x64-${{ github.sha }}
path: ${{ runner.temp }}/tested-linux-x64
- name: Download win32-x64 binary
uses: actions/download-artifact@v4
with:
name: tested-win32-x64-${{ github.sha }}
path: ${{ runner.temp }}/tested-win32-x64
- name: Download win32-arm64 binary
uses: actions/download-artifact@v4
with:
name: win32-arm64
path: packages/npm/win32-arm64/bin
- name: Verify tested macOS binary for npm
run: python3 scripts/native-release-artifact.py unpack-published --target aarch64-apple-darwin --archive "$RUNNER_TEMP/tested-darwin-arm64/release.tar" --binary packages/npm/darwin-arm64/bin/aft
- name: Verify tested Windows binary for npm
run: python3 scripts/native-release-artifact.py unpack-published --target x86_64-pc-windows-msvc --archive "$RUNNER_TEMP/tested-win32-x64/release.tar" --binary packages/npm/win32-x64/bin/aft.exe
- name: Verify tested Linux binary for npm
run: python3 scripts/native-release-artifact.py unpack-published --target x86_64-unknown-linux-gnu --archive "$RUNNER_TEMP/tested-linux-x64/release.tar" --binary packages/npm/linux-x64/bin/aft
- name: Set binary permissions
run: |
chmod +x packages/npm/darwin-arm64/bin/aft
chmod +x packages/npm/darwin-x64/bin/aft
chmod +x packages/npm/linux-arm64/bin/aft
chmod +x packages/npm/linux-x64/bin/aft
- name: Sync versions from tag
run: node scripts/version-sync.mjs --from-tag
- name: Validate packages
run: node scripts/validate-packages.mjs
- name: Install workspace dependencies
run: bun install --frozen-lockfile
- name: Build aft-bridge
run: bun run build
working-directory: packages/aft-bridge
- name: Build OpenCode plugin
run: bun run build
working-directory: packages/opencode-plugin
- name: Build Pi plugin
run: bun run build
working-directory: packages/pi-plugin
- name: Build CLI
run: bun run build
working-directory: packages/aft-cli
- name: Fetch final v0.48.1 activation baseline
run: git fetch --force origin refs/tags/v0.48.1:refs/tags/v0.48.1
- name: Prepare publication records from the tagged candidate
run: node scripts/release-gate-v049.mjs --prepare --snapshot-dir "${{ runner.temp }}/governed-publication" --candidate
- name: Verify v0.49 artifact and release gates
run: node scripts/release-gate-v049.mjs --snapshot-dir "${{ runner.temp }}/governed-publication" --candidate --require-platform-artifacts
- name: Retain governed publication records
uses: actions/upload-artifact@v4
with:
name: governed-publication-${{ github.ref_name }}
path: ${{ runner.temp }}/governed-publication/*.json
if-no-files-found: error
# Uses npm Trusted Publishing (OIDC) — configured per-package on npmjs.com
- name: Publish platform packages
run: |
set -euo pipefail
publish_or_skip() {
local pkg_dir="$1"
local pkg_name
local version
pkg_name=$(node -p "require('./${pkg_dir}/package.json').name")
version=$(node -p "require('./${pkg_dir}/package.json').version")
if npm view "${pkg_name}@${version}" version >/dev/null 2>&1; then
echo "::notice::${pkg_name}@${version} already published; skipping"
return 0
fi
npm publish --access public --provenance "$pkg_dir"
}
for pkg in darwin-arm64 darwin-x64 linux-arm64 linux-x64 win32-arm64 win32-x64; do
publish_or_skip "packages/npm/$pkg"
done
# aft-bridge MUST publish before the plugins, because @cortexkit/aft-opencode
# and @cortexkit/aft-pi depend on it. Plugins publish from local builds, so
# there's no install-time race here, but consumers installing @latest will
# need aft-bridge resolvable on npm.
#
# Tolerate `already published` so a re-run of the same tag (or the
# initial bootstrap publish that claims the package name with a
# token before Trusted Publishing is configured) doesn't fail the
# whole release. Mirrors the crates.io publish step above.
- name: Publish @cortexkit/aft-bridge
run: |
set -euo pipefail
PKG_NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
if npm view "${PKG_NAME}@${VERSION}" version >/dev/null 2>&1; then
echo "::notice::${PKG_NAME}@${VERSION} already published; skipping"
exit 0
fi
npm publish --access public --provenance
working-directory: packages/aft-bridge
- name: Publish @cortexkit/aft-opencode
run: |
set -euo pipefail
PKG_NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
if npm view "${PKG_NAME}@${VERSION}" version >/dev/null 2>&1; then
echo "::notice::${PKG_NAME}@${VERSION} already published; skipping"
exit 0
fi
npm publish --access public --provenance
working-directory: packages/opencode-plugin
- name: Publish @cortexkit/aft-pi
run: |
set -euo pipefail
PKG_NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
if npm view "${PKG_NAME}@${VERSION}" version >/dev/null 2>&1; then
echo "::notice::${PKG_NAME}@${VERSION} already published; skipping"
exit 0
fi
npm publish --access public --provenance
working-directory: packages/pi-plugin
- name: Publish @cortexkit/aft (unified CLI)
run: |
set -euo pipefail
PKG_NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
if npm view "${PKG_NAME}@${VERSION}" version >/dev/null 2>&1; then
echo "::notice::${PKG_NAME}@${VERSION} already published; skipping"
exit 0
fi
npm publish --access public --provenance
working-directory: packages/aft-cli
# GitHub release must complete before npm publish so that checksums.sha256
# is available when freshly-installed @cortexkit/aft-opencode@latest triggers
# ensureBinary(). crates.io publish is also gated so that a failed crate
# publish blocks the release — no point shipping binaries if the Rust crate
# isn't available yet.
github-release:
name: Create GitHub Release
if: github.event_name != 'workflow_dispatch'
runs-on: ubuntu-latest
needs:
- gate
- build-darwin-arm64
- build-darwin-x64
- build-linux-arm64
- build-linux-x64
- build-win32-arm64
- build-win32-x64
- e2e-darwin-arm64
- e2e-win32-x64
- e2e-linux-x64
- publish-crates
steps:
- uses: actions/checkout@v5
- name: Download tested macOS artifact
uses: actions/download-artifact@v4
with:
name: tested-darwin-arm64-${{ github.sha }}
path: ${{ runner.temp }}/tested-darwin-arm64
- name: Download tested Windows artifact
uses: actions/download-artifact@v4
with:
name: tested-win32-x64-${{ github.sha }}
path: ${{ runner.temp }}/tested-win32-x64
- name: Download tested Linux artifact
uses: actions/download-artifact@v4
with:
name: tested-linux-x64-${{ github.sha }}
path: ${{ runner.temp }}/tested-linux-x64
- name: Download macOS x64 binary
uses: actions/download-artifact@v4
with:
name: darwin-x64
path: artifacts/darwin-x64
- name: Download macOS x64 dSYM
uses: actions/download-artifact@v4
with:
name: darwin-x64-dsym
path: artifacts/darwin-x64-dsym
- name: Download Linux ARM64 binary
uses: actions/download-artifact@v4
with:
name: linux-arm64
path: artifacts/linux-arm64
- name: Download Windows ARM64 binary
uses: actions/download-artifact@v4
with:
name: win32-arm64
path: artifacts/win32-arm64
- name: Verify tested macOS binary for release
run: python3 scripts/native-release-artifact.py unpack-published --target aarch64-apple-darwin --archive "$RUNNER_TEMP/tested-darwin-arm64/release.tar" --binary release-assets/aft-darwin-arm64 --dsym release-assets/aft-darwin-arm64-dsym.zip
- name: Verify tested Windows binary for release
run: python3 scripts/native-release-artifact.py unpack-published --target x86_64-pc-windows-msvc --archive "$RUNNER_TEMP/tested-win32-x64/release.tar" --binary release-assets/aft-win32-x64.exe
- name: Verify tested Linux binary for release
run: python3 scripts/native-release-artifact.py unpack-published --target x86_64-unknown-linux-gnu --archive "$RUNNER_TEMP/tested-linux-x64/release.tar" --binary release-assets/aft-linux-x64
- name: Prepare release assets
run: |
mkdir -p release-assets
cp artifacts/darwin-x64/aft release-assets/aft-darwin-x64
cp artifacts/linux-arm64/aft release-assets/aft-linux-arm64
cp artifacts/win32-arm64/aft.exe release-assets/aft-win32-arm64.exe
chmod +x release-assets/aft-*
# Split debug info (dSYM) for the darwin release binaries, so a
# deployed binary's thread-stack samples symbolicate by name instead
# of returning nameless offsets. Named aft-<target>-dsym.zip.
cp artifacts/darwin-x64-dsym/aft-darwin-x64-dsym.zip release-assets/
# Alpha-distribution asset contract (ck setup / ck upgrade): the installer
# spec fixes the managed binary name as ck-aft, so the archives are
# ck-aft-<os>-<arch>.zip with ck-aft (ck-aft.exe on Windows) at the
# archive root, beside <name>.zip.sha256 holding the shasum -a 256 line
# for the ZIP bytes. The release inventory gate keys on these exact
# names. Additive: the bare aft-* binary assets above stay untouched for
# existing consumers (npm resolver, binary downloader, fleet staging).
- name: Package alpha distribution zips
run: |
cd release-assets
for target in darwin-arm64 darwin-x64 linux-arm64 linux-x64; do
mkdir -p "zip-stage-$target"
cp "aft-$target" "zip-stage-$target/ck-aft"
chmod +x "zip-stage-$target/ck-aft"
(cd "zip-stage-$target" && zip -q "../ck-aft-$target.zip" ck-aft)
rm -rf "zip-stage-$target"
shasum -a 256 "ck-aft-$target.zip" > "ck-aft-$target.zip.sha256"
done
# The installer spells Windows targets windows-<arch>; the bare-binary
# assets keep their historical win32-<arch> names.
for arch in arm64 x64; do
mkdir -p "zip-stage-windows-$arch"
cp "aft-win32-$arch.exe" "zip-stage-windows-$arch/ck-aft.exe"
(cd "zip-stage-windows-$arch" && zip -q "../ck-aft-windows-$arch.zip" ck-aft.exe)
rm -rf "zip-stage-windows-$arch"
shasum -a 256 "ck-aft-windows-$arch.zip" > "ck-aft-windows-$arch.zip.sha256"
done
ls -la ./*.zip ./*.zip.sha256
- name: Generate checksums
run: |
cd release-assets
sha256sum aft-* > checksums.sha256
cat checksums.sha256
- name: Verify curated release notes exist
run: |
notes_file=".alfonso/release-notes/${GITHUB_REF_NAME}.md"
if [ ! -f "$notes_file" ]; then
echo "::error::Curated release notes missing: $notes_file"
echo "Draft them under .alfonso/release-notes/ before tagging the release."
exit 1
fi
echo "Using $notes_file as the release body."
wc -l "$notes_file"