-
-
Notifications
You must be signed in to change notification settings - Fork 52
901 lines (823 loc) · 39 KB
/
Copy pathrelease.yml
File metadata and controls
901 lines (823 loc) · 39 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
name: release
on:
push:
tags:
- 'v*'
workflow_dispatch:
permissions:
contents: read
env:
RUST_LOG: info
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
jobs:
# ── Linux ─────────────────────────────────────────────────────────────
build-linux:
name: build Linux ${{ matrix.arch }}
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
runner: ubuntu-22.04
target: x86_64-unknown-linux-gnu
- arch: aarch64
runner: ubuntu-22.04-arm
target: aarch64-unknown-linux-gnu
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
TMPDIR: ${{ github.workspace }}/tmp
# The full product's cargo features, shared by the compile step and the
# bootstrap below so both describe one build. `video` is what this
# platform declares in [workspace.metadata.neomacs-production-capabilities];
# a test pins this list to that table.
RELEASE_FEATURES: video,neomacs-layout-engine/freetype-bundled
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Install system dependencies (apt)
run: scripts/ci/setup-linux.sh release
- name: Prepare workspace temp directory
run: mkdir -p "$TMPDIR"
- name: Set up Rust tooling
uses: ./.github/actions/setup-rust
with:
cache-key: release-linux-${{ matrix.arch }}
target: ${{ matrix.target }}
- name: Install linuxdeploy
uses: AnimMouse/setup-appimage@c8debfb91e47b03bf4bdc78a4b22fe14ebfaf7f5 # v2
with:
filename: linuxdeploy
url: https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${{ matrix.arch }}.AppImage
cache_key: continuous-${{ matrix.arch }}
- name: Install appimagetool
uses: AnimMouse/setup-appimage@c8debfb91e47b03bf4bdc78a4b22fe14ebfaf7f5 # v2
with:
filename: appimagetool
url: https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${{ matrix.arch }}.AppImage
cache_key: continuous-${{ matrix.arch }}
# Split so a compile failure is distinguishable from a bootstrap failure,
# and so the expensive half is a step of its own. This build must match
# xtask's own EXACTLY - same package, same features, same profile - or the
# bootstrap below rebuilds it instead of reusing it, and where nothing
# verifies the result it blesses a product missing a capability the
# manifest promises (macOS shipped without `webview` that way). Hence one
# RELEASE_FEATURES for both steps. crates/neomacs/build.rs declares
# rerun-if-env-changed on NEOMACS_BUILD_PROFILE, so the profile has to
# agree the same way. `-p neomacs` covers neomacs, neomacsclient and
# mock-display; they are bins of one package.
- name: Compile release binaries
env:
NEOMACS_BUILD_PROFILE: release
run: >-
cargo build -p neomacs
--features "$RELEASE_FEATURES"
--profile release
- name: Bootstrap and dump
run: cargo xtask fresh-build --release --features "$RELEASE_FEATURES" --skip-build
- name: Set version env
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
- name: Package tarball
run: |
# package-release.sh builds the canonical release tree
# (bin/ + share/neomacs/) and its tarball: the archive is runnable
# in place and install.sh consumes the same layout.
./scripts/package-release.sh \
--target "${{ matrix.target }}" \
--skip-build --no-smoke
- name: Package .deb
run: |
./scripts/package-deb.sh \
--target "${{ matrix.target }}" \
--skip-build --no-smoke
# No .rpm here: this runner is Ubuntu, and rpmbuild derives a package's
# library Requires from the host's rpm and libraries, so an RPM built
# here encodes Ubuntu's ncurses and glibc symbol versions and is
# unsatisfiable on Fedora (issue #388). `build-linux-rpm` builds it
# inside el9 instead.
# The AppImage carries the same product as the tarball, so linuxdeploy
# bundles its GStreamer closure into the image; codec plugin families
# still come from the host, as they do for the tar/deb/rpm artifacts.
- name: Package AppImage
run: |
./scripts/package-appimage.sh \
--target "${{ matrix.target }}" \
--skip-build --no-smoke
# --formats: the audit hard-errors on a requested format that is absent,
# and this job no longer produces the .rpm (`build-linux-rpm` does, and
# audits it there).
#
# --tar-version only on a tag: the value comes from GITHUB_REF_NAME, which
# on a `workflow_dispatch` run is the BRANCH name, and the audit then looks
# for neomacs-<branch>-<triple>.tar.gz, finds nothing and fails. That made
# every dispatch of this workflow red in this step -- which is the one way
# to exercise a release-workflow change without cutting a tag.
- name: Verify packaged artifacts and GLIBC baseline
run: |
# Quote the format list: it is ONE argument whose value happens to
# contain commas, and shellcheck's SC2054 ("use spaces, not commas,
# to separate array elements") cannot tell that from three elements.
# Quoting states the intent and is what silences it.
args=(--target "${{ matrix.target }}" --formats "tar,appimage,deb")
if [[ "${{ github.ref_type }}" == "tag" ]]; then
args+=(--tar-version "$VERSION")
fi
./scripts/test-linux-release-artifacts.sh "${args[@]}"
- name: Upload Linux artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: linux-${{ matrix.arch }}
path: |
dist/*.tar.gz
dist/*.AppImage
dist/*.deb
if-no-files-found: error
# ── RPM (built and packaged inside el9, its target distro) ────────────
#
# Why this is not built on the ubuntu runner: rpmbuild derives each package's
# library Requires from the payload's ELF *and* the build host's rpm, so an
# Ubuntu-built binary records Ubuntu's ncurses and glibc symbol versions:
#
# nothing provides libtinfo.so.6(NCURSES6_TINFO_5.0.19991023)(64bit)
#
# Fedora publishes no such symbol version, so the package was uninstallable
# there (issue #388). Building inside el9 makes el9's glibc (2.34) and
# unversioned ncurses the recorded requirements, so ONE artifact installs on
# RHEL 9 / Rocky / Alma and upward on Fedora 43+, and it still satisfies the
# GLIBC 2.35 ceiling that `test-linux-release-artifacts.sh` enforces.
build-linux-rpm:
name: build RPM el9 ${{ matrix.arch }}
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
runner: ubuntu-22.04
target: x86_64-unknown-linux-gnu
- arch: aarch64
runner: ubuntu-22.04-arm
target: aarch64-unknown-linux-gnu
runs-on: ${{ matrix.runner }}
# The image is pinned by index digest, like the Dockerfiles under docker/:
# this entire job's correctness rests on el9's ncurses having no symbol
# version stamps and its glibc being 2.34, so the image that was verified is
# the image that runs. It is a multi-arch index, so each runner pulls its
# own architecture and nothing runs under emulation.
container:
image: quay.io/almalinuxorg/almalinux:9@sha256:3da647417303590fea18439c87421a886d4245fef358cf131d0e0ce7b23f4236
timeout-minutes: 120
# The default shell inside a container job is `sh`; bash also buys the
# `-o pipefail` the other jobs get from the runner's bash template.
defaults:
run:
shell: bash
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
# Same reason as build-linux: keep large temporary files on the workspace
# volume rather than wherever the container's /tmp lands.
TMPDIR: ${{ github.workspace }}/tmp
# Must match build-linux exactly: xtask's bootstrap reuses the compile
# artifact only when package, features and profile agree.
RELEASE_FEATURES: video,neomacs-layout-engine/freetype-bundled
steps:
# TMPDIR must exist before the first dnf runs -- dnf honors TMPDIR, the job
# points it at the workspace, and dnf dies without it:
# Cannot create temporary file - mkstemp .../tmp/librepo-tmp-XXXX:
# No such file or directory
# (mkdir is in the base image; git is not, hence the install below.)
- name: Prepare workspace temp directory (for the dnf below)
run: mkdir -p "$TMPDIR"
# The image ships no git, and `actions/checkout` reaches for it. No
# `curl` here on purpose: the image ships `curl-minimal`, which already
# provides the binary, and asking for the full package is a hard conflict
# on el9 ("curl-minimal conflicts with curl provided by curl").
- name: Install git for checkout
run: dnf install -y git tar gzip which
# Before checkout, not after: the container runs as root while the mounted
# workspace may be owned by another uid, and git refuses a repository it
# does not consider owned without this.
- name: Mark the workspace safe for git
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # package-release.sh reads the version from git tags
# Again, because checkout cleans the workspace first (`git clean -ffdx`
# with its default `clean: true`, and -x removes ignored paths like
# tmp/). Without this the very next dnf step fails on mkstemp, which is
# how the first CI dispatch of this job spent its time.
- name: Prepare workspace temp directory (for the build)
run: mkdir -p "$TMPDIR"
# el9 package names, derived from the workspace's own pkg-config and
# build.rs probes rather than translated from scripts/ci/setup-linux.sh:
# most of that Ubuntu list is dead weight here (X11, Wayland and Vulkan
# are dlopen'ed; openssl, gnutls, libxml2, cairo and pango are not
# dependencies at all). Three entries are easy to miss and mandatory:
# libdrm-devel gstreamer-allocators-sys links drm (the .pc file passes
# -ldrm), which the payload's DT_NEEDED does not reveal
# dbus-devel neovm-core depends on `dbus` for every unix but macOS,
# and `libdbus-sys`'s build script `panic!()`s outright
# when `dbus-1.pc` is missing -- it has no HAVE_DBUS=no
# mode. Ubuntu's list gets this as `libdbus-1-dev`
# (scripts/ci/setup-linux.sh); el9 calls it dbus-devel.
# gawk neovm-core/build.rs generates Lisp with awk and panics
# without GNU awk
# gcc-c++ simdutf (via rio-vt) compiles C++ and links libstdc++
# diffutils the artifact audit shells out to cmp; a minimal image
# has no cmp, only the Ubuntu runner did
# The second install is the runtime closure: the packaging scripts execute
# the binary they stage, and rpmbuild runs its brp scripts over it.
- name: Install build and packaging dependencies (dnf)
run: |
# Everything below resolves from BaseOS/AppStream, and the AlmaLinux
# image enables CRB by default -- the enable is kept only for images
# that ship it off, and it is not allowed to fail the job.
dnf install -y dnf-plugins-core
dnf config-manager --set-enabled crb || echo "crb: not enabled (not needed for this list)"
dnf install -y \
make binutils cpio diffutils file gcc gcc-c++ pkgconf-pkg-config \
gawk ncurses-devel fontconfig-devel freetype-devel glib2-devel \
gstreamer1-devel gstreamer1-plugins-base-devel libdrm-devel \
dbus-devel \
zlib-devel lcms2-devel rpm-build
dnf install -y gstreamer1 gstreamer1-plugins-base glib2 fontconfig \
freetype ncurses-libs libstdc++ zlib libdrm dbus-libs
- name: Set up Rust tooling
uses: ./.github/actions/setup-rust
with:
cache-key: release-linux-rpm-el9-${{ matrix.arch }}
target: ${{ matrix.target }}
- name: Compile release binaries
env:
NEOMACS_BUILD_PROFILE: release
run: >-
cargo build -p neomacs
--features "$RELEASE_FEATURES"
--profile release
- name: Bootstrap and dump
run: cargo xtask fresh-build --release --features "$RELEASE_FEATURES" --skip-build
# `%{?dist}` is defined here, so the file is
# neomacs-<version>-1.el9.<arch>.rpm.
- name: Package .rpm
run: |
./scripts/package-rpm.sh \
--target "${{ matrix.target }}" \
--skip-build --no-smoke
# The point of the whole job: the package's own declared dependencies,
# resolved by a real transaction on the distro it targets. This is the
# check that issue #388 needed and nothing performed. A package whose
# requirements el9 cannot satisfy -- a versioned ncurses/tinfo symbol, say
# -- fails right here.
#
# The explicit assertion on that symbol version lives in
# test-linux-release-artifacts.sh (the audit step below), not here, so a
# local run exercises it too; it also is what names the requirement in the
# failure output.
- name: Install the package on el9 (no --nodeps)
run: |
dnf install -y ./dist/*.rpm
/usr/bin/neomacs --version
timeout 60 /usr/bin/neomacs --batch --eval "(kill-emacs 0)"
# No --tar-version here: this job's dist/ holds exactly one tarball, and
# pinning it to ${GITHUB_REF_NAME#v} would break a `workflow_dispatch` run
# from a branch, where that is the branch name and not a version.
- name: Verify packaged artifacts and GLIBC baseline
run: |
./scripts/test-linux-release-artifacts.sh \
--target "${{ matrix.target }}" \
--formats tar,rpm
- name: Upload RPM artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: rpm-el9-${{ matrix.arch }}
path: dist/*.rpm
if-no-files-found: error
# ── Fedora install guard ──────────────────────────────────────────────
#
# The el9 RPM exists to be installable on Fedora; this installs the built
# artifact there, on a real Fedora, with no --nodeps. Without it the release
# could ship an unsatisfiable package again and only a user would notice.
#
# x86_64 only: an aarch64 Fedora container would have to emulate, so the
# aarch64 RPM is not covered by this check. Its Requires come from the same
# spec and the same el9 build, and `build-linux-rpm` installs it on el9, so
# the exposure is a Fedora-aarch64-specific packaging difference.
verify-rpm-on-fedora:
name: verify RPM installs on Fedora
needs:
- build-linux-rpm
runs-on: ubuntu-24.04
container:
image: registry.fedoraproject.org/fedora:44
timeout-minutes: 20
defaults:
run:
shell: bash
steps:
- name: Install extraction tools for the artifact download
run: dnf install -y tar unzip
- name: Download the RPM built on el9
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: rpm-el9-x86_64
path: dist
- name: Install it on Fedora and run it
run: |
ls -l ./dist/*.rpm
dnf install -y ./dist/*.rpm
neomacs --version
timeout 60 neomacs --batch --eval "(kill-emacs 0)"
# ── macOS aarch64 (Apple Silicon) ─────────────────────────────────────
build-macos-aarch64:
name: build macOS aarch64
# Pin the image so a release does not change underneath us when GitHub
# advances `macos-latest`. GitHub's standard macos-15 image is arm64.
runs-on: macos-15
timeout-minutes: 180
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
# vendor-macos-runtime.sh rewrites every non-system load command to
# @executable_path/../Frameworks/..., which is LONGER than the original
# install name. Mach-O has no room to grow those load commands unless
# the linker reserved it, so install_name_tool failed on mock-display
# with "larger updated load commands do not fit (the program must be
# relinked, and you may need to use -headerpad_max_install_names)".
# Reserve the space for every binary that goes into the bundle -- the
# others only fit by accident of their existing padding.
RUSTFLAGS: "-C link-arg=-Wl,-headerpad_max_install_names"
# Shared by the compile step and the bootstrap below so both describe one
# build. `webview` is what darwin declares in
# [workspace.metadata.neomacs-production-capabilities]; a test pins this
# list to that table. Omitting it here is what shipped macOS without the
# WKWebView layer, because verify_built_product only inspects Linux.
RELEASE_FEATURES: webview,neomacs-layout-engine/freetype-bundled
outputs:
distribution_mode: ${{ steps.macos-signing.outputs.distribution_mode }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up Rust tooling
uses: ./.github/actions/setup-rust
with:
cache-key: release-macos-aarch64
target: aarch64-apple-darwin
- name: Set version env
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
# Split so a compile failure is distinguishable from a bootstrap failure,
# and so the expensive half is a step of its own. The args must match
# xtask's initial_cargo_build_args EXACTLY - it builds `-p neomacs` with
# these features under this profile, and crates/neomacs/build.rs declares
# rerun-if-env-changed=NEOMACS_BUILD_PROFILE - or the second step would
# refingerprint and rebuild instead of reusing this. `-p neomacs` covers
# neomacs, neomacsclient and mock-display; they are bins of one package.
- name: Compile release binaries
env:
NEOMACS_BUILD_PROFILE: release
run: cargo build -p neomacs --features "$RELEASE_FEATURES" --profile release
- name: Bootstrap and dump
run: cargo xtask fresh-build --release --features "$RELEASE_FEATURES" --skip-build
- name: Configure Developer ID signing and notarization
id: macos-signing
env:
# Developer ID is an optional trust upgrade. Set this repository
# variable to 1 only after all five secrets below are configured.
MACOS_REQUIRE_SIGNING: ${{ vars.MACOS_REQUIRE_SIGNING || '0' }}
MACOS_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARY_KEY_P8_BASE64: ${{ secrets.APPLE_NOTARY_KEY_P8_BASE64 }}
APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }}
APPLE_NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }}
run: ./scripts/configure-macos-signing-ci.sh
- name: Build and package complete macOS application
run: ./scripts/package-macos-app.sh --skip-build
- name: Upload notarization diagnostics
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: macos-notarization-diagnostics-aarch64
path: dist/notary/*.json
if-no-files-found: ignore
- name: Upload macOS aarch64 artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: macos-aarch64
path: |
dist/*.dmg
dist/*.zip
dist/*.tar.gz
if-no-files-found: error
# ── macOS x86_64 (Intel) ──────────────────────────────────────────────
# `macos-15-intel` is GitHub's LAST x86_64 runner image, promised only
# until August 2027 (actions/runner-images#13045); macos-13, the previous
# Intel image, closed down in December 2025. Build this target natively
# while the image exists -- afterwards the only path is cross-compiling
# x86_64-apple-darwin on an arm64 runner and running the bootstrap's pdump
# generation under Rosetta.
build-macos-x86_64:
name: build macOS x86_64
runs-on: macos-15-intel
timeout-minutes: 180
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
# See the aarch64 job: vendoring rewrites load commands to longer
# @executable_path names, so every linked binary needs the reserved
# header space.
RUSTFLAGS: "-C link-arg=-Wl,-headerpad_max_install_names"
# Shared by the compile step and the bootstrap below so both describe
# one build. darwin declares the same cargo-features for either
# architecture; a test pins this list to that table.
RELEASE_FEATURES: webview,neomacs-layout-engine/freetype-bundled
outputs:
distribution_mode: ${{ steps.macos-signing.outputs.distribution_mode }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up Rust tooling
uses: ./.github/actions/setup-rust
with:
cache-key: release-macos-x86_64
target: x86_64-apple-darwin
- name: Set version env
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
# Same contract as the aarch64 job: the args must match xtask's
# initial_cargo_build_args EXACTLY, including NEOMACS_BUILD_PROFILE,
# or the bootstrap refingerprints and rebuilds instead of reusing.
- name: Compile release binaries
env:
NEOMACS_BUILD_PROFILE: release
run: cargo build -p neomacs --features "$RELEASE_FEATURES" --profile release
- name: Bootstrap and dump
run: cargo xtask fresh-build --release --features "$RELEASE_FEATURES" --skip-build
- name: Configure Developer ID signing and notarization
id: macos-signing
env:
# The Developer ID certificate and App Store Connect key are
# architecture-agnostic; the same secrets sign both macOS jobs.
MACOS_REQUIRE_SIGNING: ${{ vars.MACOS_REQUIRE_SIGNING || '0' }}
MACOS_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARY_KEY_P8_BASE64: ${{ secrets.APPLE_NOTARY_KEY_P8_BASE64 }}
APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }}
APPLE_NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }}
run: ./scripts/configure-macos-signing-ci.sh
- name: Build and package complete macOS application
run: ./scripts/package-macos-app.sh --skip-build
- name: Upload notarization diagnostics
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: macos-notarization-diagnostics-x86_64
path: dist/notary/*.json
if-no-files-found: ignore
- name: Upload macOS x86_64 artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: macos-x86_64
path: |
dist/*.dmg
dist/*.zip
dist/*.tar.gz
if-no-files-found: error
# Test on a second, clean machine so the SDK installed by the build cannot
# satisfy an accidentally external dependency or runtime resource lookup.
build-windows:
name: build Windows ${{ matrix.arch }}
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
runner: windows-latest
target: x86_64-pc-windows-msvc
- arch: aarch64
runner: windows-11-arm
target: aarch64-pc-windows-msvc
runs-on: ${{ matrix.runner }}
timeout-minutes: 180
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
NEOMACS_RELEASE_TARGET: ${{ matrix.target }}
# Shared by the compile step and the bootstrap below so both describe one
# build. Include the Windows helper capability declared in
# [workspace.metadata.neomacs-production-capabilities].
RELEASE_FEATURES: windows-tools,neomacs-layout-engine/freetype-bundled
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up Rust tooling
uses: ./.github/actions/setup-rust
with:
cache-key: release-windows-msvc-${{ matrix.arch }}
install-nextest: true
target: ${{ matrix.target }}
- name: Install NSIS
run: choco install nsis -y
- name: Add NSIS to PATH
shell: powershell
run: Add-Content -Path $env:GITHUB_PATH -Value "C:\Program Files (x86)\NSIS"
# Split so a compile failure is distinguishable from a bootstrap failure.
# The args must match xtask's initial_cargo_build_args EXACTLY, including
# NEOMACS_BUILD_PROFILE, which crates/neomacs/build.rs declares
# rerun-if-env-changed on - otherwise the bootstrap step refingerprints
# and rebuilds instead of reusing this.
- name: Compile release binaries
shell: bash
timeout-minutes: 150
env:
NEOMACS_BUILD_PROFILE: release
run: |
cargo build -p neomacs --features "$RELEASE_FEATURES" --profile release
- name: Bootstrap and dump
shell: bash
timeout-minutes: 60
run: |
# The pdump generation runs neomacs, whose startup expands `~`. The
# minimal CI build environment sets USERPROFILE/APPDATA but not HOME,
# so `~` would stay literal and `directory-files "~"` would fail. Give
# it HOME explicitly (a real Windows session always has one; GNU's
# w32.c init_environment likewise guarantees HOME is set).
export HOME="${USERPROFILE:-$GITHUB_WORKSPACE}"
cargo xtask fresh-build --release --features "$RELEASE_FEATURES" --skip-build
- name: Set version env (bash)
shell: bash
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
- name: Package zip
shell: bash
run: |
TARGET="$NEOMACS_RELEASE_TARGET"
STAGING="neomacs-${VERSION}-${TARGET}"
mkdir -p "$STAGING"
cp target/release/neomacs.exe "$STAGING/"
cp target/release/neomacsclient.exe "$STAGING/"
cp target/release/cmdproxy.exe "$STAGING/"
cp target/release/runneomacs.exe "$STAGING/"
cp target/release/neomacs.pdump "$STAGING/"
cp -r lisp "$STAGING/"
cp -r etc "$STAGING/"
cp COPYING "$STAGING/"
mkdir -p dist
7z a -tzip "dist/${STAGING}.zip" "$STAGING"
- name: Package .exe installer
shell: bash
timeout-minutes: 20
run: |
./scripts/package-windows-installer.sh \
--target "$NEOMACS_RELEASE_TARGET" \
--skip-build --no-smoke
- name: Verify Windows installer ownership contract
shell: powershell
timeout-minutes: 10
run: |
./scripts/run-windows-installer-contract.ps1 `
-Architecture "${{ matrix.arch }}" `
-ConfirmEphemeralRunner
- name: Upload Windows artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: windows-${{ matrix.arch }}
path: |
dist/*.zip
dist/*.exe
if-no-files-found: error
# ── GitHub Release ────────────────────────────────────────────────────
create-release:
name: create release
# Same guard the three publishing jobs below already carry. Without it a
# `workflow_dispatch` run -- which is how a release workflow gets validated
# without cutting a tag -- would publish a release named after the branch,
# and `verify-install-script` would then try to verify it. With it, a
# dispatch builds and verifies everything (including the el9 RPM and the
# Fedora install) and publishes nothing; only a `v*` tag push releases.
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
discussions: write
needs:
- build-linux
# The RPM comes from el9, not from build-linux, so the release waits for
# it here or the artifact list would be missing a package the notes link.
- build-linux-rpm
# And it waits for the Fedora install to succeed: that job exists to catch
# a recurrence of issue #388, which it cannot do if a failing install
# still publishes the release.
- verify-rpm-on-fedora
- build-macos-aarch64
- build-macos-x86_64
- build-windows
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download all artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: dist
merge-multiple: true
- name: Stage the installer and its checksum manifest
run: |
# install.sh ships as a release asset so that
# https://github.com/eval-exec/neomacs/releases/latest/download/install.sh
# always serves the installer that matches the assets it downloads
# (the neomacs.org entry point fetches exactly that URL).
install -m 0644 install.sh dist/install.sh
# The glob expands before SHA256SUMS exists, so the manifest covers
# every asset (and the installer) but never itself.
cd dist
sha256sum ./* > SHA256SUMS
- name: Generate GitHub changelog
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api \
--method POST \
"repos/$GITHUB_REPOSITORY/releases/generate-notes" \
-f tag_name="$GITHUB_REF_NAME" \
-f target_commitish="$GITHUB_SHA" \
--jq .body > generated-release-notes.md
- name: Generate release body
run: |
./scripts/generate-release-notes.sh \
--repo "$GITHUB_REPOSITORY" \
--tag "$GITHUB_REF_NAME" \
--dist-dir dist \
--generated-notes generated-release-notes.md \
--output release-notes.md
- name: Create GitHub Release
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
with:
name: NEO Emacs ${{ github.ref_name }}
body_path: release-notes.md
files: dist/*
discussion_category_name: Announcements
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# The reusable publisher also provides the manual, release-tag backfill path.
# Future releases and backfills therefore share exactly one cache policy.
publish-cachix-release:
name: publish Nix release closures
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
needs: create-release
permissions:
contents: read
uses: ./.github/workflows/cachix-release.yml
with:
release_tag: ${{ github.ref_name }}
release_commit: ${{ github.sha }}
secrets:
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
# Notify the independently owned AUR publisher only after every native asset
# and SHA256SUMS have become public. The downstream repository retains its
# own AUR SSH credential and publishing policy; this job can only send the
# versioned release fact. Its six-hour reconciliation schedule recovers a
# missed dispatch without coupling AUR availability to release creation.
notify-aur-publisher:
name: notify AUR publisher
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
needs: create-release
permissions:
contents: read
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Dispatch exact stable release
env:
# Fine-grained token or GitHub App token scoped to repository
# Irfrit/neomacs-aur. It needs only the target permission required by
# the repository-dispatch endpoint; never store the AUR SSH key here.
GH_TOKEN: ${{ secrets.NEOMACS_AUR_DISPATCH_TOKEN }}
run: |
./scripts/dispatch-release-event.sh \
--target-repository Irfrit/neomacs-aur \
--source-repository "$GITHUB_REPOSITORY" \
--tag "$GITHUB_REF_NAME" \
--commit "$GITHUB_SHA"
# Package Docker images only after the native GitHub release and checksum
# manifest exist. The reusable workflow also supports manual backfills of
# an already-published release such as v0.0.15.
publish-docker:
name: publish Docker release
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
needs: create-release
permissions:
contents: read
packages: write
uses: ./.github/workflows/docker-release.yml
with:
release_tag: ${{ github.ref_name }}
secrets:
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
# ── Install script verification ───────────────────────────────────────
# Installs through the public curl|bash entry exactly as a user would:
# the installer asset is downloaded from the just-published release, and
# its own post-install check batch-starts the binary with no environment
# variables, exercising the runtime-root resolution through the
# ~/.local/bin symlink chain.
verify-macos-install-script:
name: verify install.sh on macOS ${{ matrix.arch }} from the published release
# This MUST depend on create-release: it reads an asset OF the release.
# It previously ran inside a job that create-release itself depended on, so
# the release could not be made until a step passed that could only pass
# once the release existed - a cycle that can never be satisfied.
#
# install.sh selects the asset by uname, so each arch entry verifies its
# own published tarball end to end; the x86_64 entry rides GitHub's last
# Intel image (until August 2027, actions/runner-images#13045).
strategy:
fail-fast: false
matrix:
include:
- arch: aarch64
runner: macos-latest
- arch: x86_64
runner: macos-15-intel
needs: create-release
runs-on: ${{ matrix.runner }}
timeout-minutes: 20
steps:
- name: Run the published installer
run: |
# Same user path as the Linux verify-install-script job: fetch the
# installer asset from the just-published release and let its own
# post-install check batch-start the app bundle's binary through
# the ~/.local/bin symlink (no environment variables).
curl -fsSL \
"https://github.com/eval-exec/neomacs/releases/download/${GITHUB_REF_NAME}/install.sh" \
-o install.sh
sh install.sh --tag "${GITHUB_REF_NAME}"
test -x "$HOME/.local/bin/neomacs"
env -u NEOMACS_RUNTIME_ROOT \
"$HOME/.local/bin/neomacs" --batch --eval '(princ (emacs-version))'
verify-install-script:
name: verify install.sh from the published release
needs: create-release
runs-on: ubuntu-24.04
timeout-minutes: 20
env:
prefix: ${{ github.workspace }}/tmp/prefix
steps:
# The Linux product declares `video`, so the tarball links GStreamer and
# needs it present to start; only the AppImage carries its own copy. The
# runtime libraries (not `-dev`) are exactly what a user installing the
# tarball needs, so installing them here is what makes this job test the
# documented path rather than whichever machine it landed on.
#
# This label resolves to both self-hosted and GitHub-hosted runners. The
# self-hosted ones have GStreamer, the GitHub-hosted image does not, and
# v0.0.17 passed only because it drew a self-hosted one; v0.0.18 drew a
# hosted one and died with `libgstpbutils-1.0.so.0: cannot open shared
# object file`.
- name: Install the GStreamer runtime the tarball links
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends -y \
libgstreamer1.0-0 libgstreamer-plugins-base1.0-0
- name: Run the published installer
run: |
curl -fsSL \
"https://github.com/eval-exec/neomacs/releases/download/${GITHUB_REF_NAME}/install.sh" \
-o install.sh
sh install.sh --tag "${GITHUB_REF_NAME}" --prefix "$prefix"
- name: Assert the installed layout
run: |
set -eux
version="${GITHUB_REF_NAME#v}"
test -x "$prefix/bin/neomacs"
test "$(readlink "$prefix/share/neomacs/current")" = "versions/$version"
test "$(readlink "$prefix/bin/neomacs")" = "../share/neomacs/current/bin/neomacs"
exe="$(readlink -f "$prefix/bin/neomacs")"
ver_dir="$(dirname "$(dirname "$exe")")"
test -d "$ver_dir/share/neomacs/lisp"
# The dump lives in the archlib -- GNU's
# ${libexecdir}/emacs/${version}/${configuration} (configure.ac:290)
# -- and `exec-directory' has to name that same directory
# (src/callproc.c:1961). Ask the installed binary rather than
# rebuilding the path here, so this asserts what it resolved.
archlib="$(
env -u NEOMACS_RUNTIME_ROOT -u EMACSPATH \
"$prefix/bin/neomacs" --batch --eval '(princ exec-directory)'
)"
case "$archlib" in
"$ver_dir"/libexec/neomacs/*/*/) ;;
*) echo "exec-directory is not the installed archlib: $archlib" >&2; exit 1 ;;
esac
test -f "$archlib/neomacs.pdump"
env -u NEOMACS_RUNTIME_ROOT \
"$prefix/bin/neomacs" --batch --eval '(princ (emacs-version))'