Repository navigation
861 lines (747 loc) · 32.3 KB
/
Copy pathrelease.yml
File metadata and controls
861 lines (747 loc) · 32.3 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
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
jobs:
# The tagged commit has already passed the unit and E2E matrix in tests.yml:
# every commit reaches main through scripts/train-push.sh, which fast-forwards
# main only after the commit's tests.yml run is green, and main's branch
# protection requires those checks. Rerunning that matrix here tested the
# same commit again and added nothing but flake exposure: GitHub reruns
# failed jobs only after the whole run finishes, so one flaky test held the
# publish behind the slowest build. This gate instead proves the commit is on
# main and its tests.yml run is green, every required check of main included
# (scripts/release-ci-gate.sh). There is no skip input: a refused tag is
# released by getting that run green and rerunning this workflow.
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
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 }}
# ---------------------------------------------------------------------------
# The six platform builds have no `needs:` and start immediately, in
# parallel with the gate. Every publishing job needs the gate and all six
# builds, so a refused gate or a failed build blocks every side effect.
# The shipped bytes are built here and only smoke-tested (macOS ARM64 and
# Windows x64 run `--version`), while the same source in the release profile
# is e2e-tested by the tests.yml run on the push to main.
# ---------------------------------------------------------------------------
build-darwin-arm64:
name: Build macOS ARM64
runs-on: macos-26
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@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@v2
with:
shared-key: release-darwin-arm64
cache-bin: false
cache-on-failure: true
- 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 -p agent-file-tools
# 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: |
ditto target/release/aft.dSYM release-dsym/aft.dSYM
ditto -c -k --keepParent release-dsym/aft.dSYM release-dsym/aft-darwin-arm64-dsym.zip
- uses: actions/upload-artifact@v4
with:
name: darwin-arm64
path: target/release/aft
if-no-files-found: error
- uses: actions/upload-artifact@v4
with:
name: darwin-arm64-dsym
path: release-dsym/aft-darwin-arm64-dsym.zip
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@v0.0.11
- uses: Swatinem/rust-cache@v2
with:
shared-key: release-darwin-x64
cache-bin: false
cache-on-failure: true
- 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@v2
with:
shared-key: release-linux-arm64
cache-on-failure: true
- 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@v2
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
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
shared-key: release-linux-x64
cache-on-failure: true
- 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@v2
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
- uses: actions/upload-artifact@v4
with:
name: linux-x64
path: target/x86_64-unknown-linux-gnu/release/aft
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@v0.0.11
- uses: Swatinem/rust-cache@v2
with:
shared-key: release-win32-arm64
cache-on-failure: true
- 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
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@v0.0.11
- uses: Swatinem/rust-cache@v2
with:
shared-key: release-win32-x64
cache-on-failure: true
- 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 x86_64-pc-windows-msvc -p agent-file-tools
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
- uses: actions/upload-artifact@v4
with:
name: win32-x64
path: target/x86_64-pc-windows-msvc/release/aft.exe
if-no-files-found: error
# 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
runs-on: ubuntu-latest
needs:
- gate
- build-darwin-arm64
- build-darwin-x64
- build-linux-arm64
- build-linux-x64
- build-win32-arm64
- build-win32-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: darwin-arm64
path: packages/npm/darwin-arm64/bin
- 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: linux-x64
path: packages/npm/linux-x64/bin
- name: Download win32-x64 binary
uses: actions/download-artifact@v4
with:
name: win32-x64
path: packages/npm/win32-x64/bin
- name: Download win32-arm64 binary
uses: actions/download-artifact@v4
with:
name: win32-arm64
path: packages/npm/win32-arm64/bin
- 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: Verify v0.49 artifact and release gates
run: node scripts/release-gate-v049.mjs --candidate --evidence docs/v0.49-release-evidence.json --require-platform-artifacts
# 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
runs-on: ubuntu-latest
needs:
- gate
- build-darwin-arm64
- build-darwin-x64
- build-linux-arm64
- build-linux-x64
- build-win32-arm64
- build-win32-x64
- publish-crates
steps:
- uses: actions/checkout@v5
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Prepare release assets
run: |
mkdir -p release-assets
cp artifacts/darwin-arm64/aft release-assets/aft-darwin-arm64
cp artifacts/darwin-x64/aft release-assets/aft-darwin-x64
cp artifacts/linux-arm64/aft release-assets/aft-linux-arm64
cp artifacts/linux-x64/aft release-assets/aft-linux-x64
cp artifacts/win32-arm64/aft.exe release-assets/aft-win32-arm64.exe
cp artifacts/win32-x64/aft.exe release-assets/aft-win32-x64.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-arm64-dsym/aft-darwin-arm64-dsym.zip release-assets/
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"
# The release is assembled with the gh CLI rather than an upload action:
# v0.56.2 failed three times in a row inside softprops/action-gh-release,
# which pushes all 21 assets (~1.1 GB) through parallel streams and
# surfaces whatever the upload service returns ("Error creating asset
# temp dir", "Headers Timeout Error") as a job failure, then deletes and
# re-uploads everything on the next attempt. Here each asset goes up on
# its own stream with bounded retries, an asset that is already present
# is replaced only if its size differs, and the draft is published only
# after the asset set on the server matches the set on disk.
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
notes_file=".alfonso/release-notes/${TAG}.md"
if ! gh release view "$TAG" --json id --jq .id >/dev/null 2>&1; then
gh release create "$TAG" --draft --title "$TAG" --notes-file "$notes_file" --verify-tag
else
gh release edit "$TAG" --draft --title "$TAG" --notes-file "$notes_file"
fi
# name<TAB>size of what the server holds now, so a completed upload
# from an earlier attempt is kept instead of pushed again.
server_assets() {
gh release view "$TAG" --json assets --jq '.assets[] | "\(.name)\t\(.size)"'
}
for path in release-assets/*; do
name=$(basename "$path")
size=$(stat -c %s "$path")
if server_assets | grep -qx "$(printf '%s\t%s' "$name" "$size")"; then
echo "present: $name ($size bytes)"
continue
fi
for attempt in 1 2 3 4; do
if gh release upload "$TAG" "$path" --clobber; then
echo "uploaded: $name ($size bytes) on attempt $attempt"
break
fi
if [ "$attempt" = 4 ]; then
echo "::error::upload of $name failed on every attempt"
exit 1
fi
echo "upload of $name failed on attempt $attempt; retrying"
sleep $((attempt * 15))
done
done
expected=$(find release-assets -mindepth 1 -maxdepth 1 -type f | wc -l)
actual=$(server_assets | wc -l)
if [ "$expected" != "$actual" ]; then
echo "::error::release holds $actual assets, expected $expected"
server_assets
exit 1
fi
for path in release-assets/*; do
name=$(basename "$path"); size=$(stat -c %s "$path")
server_assets | grep -qx "$(printf '%s\t%s' "$name" "$size")" \
|| { echo "::error::size mismatch on server for $name"; exit 1; }
done
gh release edit "$TAG" --draft=false
echo "published $TAG with $actual assets"
# Announce on Discord after the GitHub release is created.
#
# Why this lives here (not in discord-release.yml as `on: release: published`):
# GitHub Actions intentionally suppresses event-triggered workflow cascades
# from `GITHUB_TOKEN`-authored events. The release is published above with
# the default `GITHUB_TOKEN`, so the `release: published` event it emits does
# not trigger downstream workflows. Inlining the Discord post here is the
# cleanest fix (vs rotating PATs for release creation).
#
# `continue-on-error: true` keeps Discord failures from rolling back a
# successful release — if the webhook is down, the announcement can be
# retried via `gh workflow run discord-release.yml ...`.
discord-announce:
name: Announce on Discord
runs-on: ubuntu-latest
# MUST wait for BOTH github-release AND publish-npm. Without `publish-npm`
# the announcement can fire while npm packages are still uploading,
# leaving users who click the release link unable to install the
# versions Discord just announced.
needs:
- gate
- github-release
- publish-npm
if: success()
continue-on-error: true
steps:
- uses: actions/checkout@v5
- name: Read curated release notes
id: notes
run: |
notes_file=".alfonso/release-notes/${GITHUB_REF_NAME}.md"
if [ ! -f "$notes_file" ]; then
echo "::error::Curated release notes missing: $notes_file"
exit 1
fi
# Use a random delimiter to guard against any string in the notes
# accidentally matching the heredoc terminator. Also ensure the file
# ends with a newline so `cat`'s output doesn't glue onto the EOF
# marker (v0.26.3 release notes had no trailing newline, which
# produced "Matching delimiter not found" and skipped the Discord
# announcement entirely).
eof="EOF_$(openssl rand -hex 12)"
{
printf 'body<<%s\n' "$eof"
cat "$notes_file"
# Guarantee a newline before the closing delimiter, even if the
# file itself doesn't end with one.
printf '\n%s\n' "$eof"
} >> "$GITHUB_OUTPUT"
- name: Post to Discord
uses: SethCohen/github-releases-to-discord@v1
with:
webhook_url: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }}
# Cortexkit teal — decimal value of #2C4A7C.
color: "2902140"
username: "AFT Releases"
# Reduce h1/h2 headings to fit Discord embed limits comfortably.
reduce_headings: true
# Keep PR/issue links — useful context in Discord.
remove_github_reference_links: false
footer_title: "Changelog"
footer_timestamp: true
release_name: ${{ github.ref_name }}
release_body: ${{ steps.notes.outputs.body }}
release_html_url: https://github.com/${{ github.repository }}/releases/tag/${{ github.ref_name }}