release: v0.0.19 #96
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| # 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 \ | |
| zlib-devel lcms2-devel rpm-build | |
| dnf install -y gstreamer1 gstreamer1-plugins-base glib2 fontconfig \ | |
| freetype ncurses-libs libstdc++ zlib libdrm | |
| - 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: Test Windows launcher | |
| shell: bash | |
| env: | |
| NEOMACS_BUILD_PROFILE: release | |
| run: cargo nextest run -p neomacs --cargo-profile release --features "$RELEASE_FEATURES" --test windows_launcher | |
| - 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))' |