Closes #335 Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
622 lines
28 KiB
YAML
622 lines
28 KiB
YAML
# Builds moho for a pushed version tag (v1.0.0) and publishes the release,
|
|
# with the notes for that version taken from CHANGELOG.md and its files named
|
|
# after the version (#239). Every version tag publishes a full release, marked
|
|
# GitHub's "Latest" - a release candidate (v1.0.0-rc.1) included, so it is
|
|
# shown on the repository's page like any other release.
|
|
#
|
|
# Tags only. A push to master builds nothing: master and a tag on it were two
|
|
# push events, so every release was built twice. To check a branch without
|
|
# releasing it, run this by hand from the Actions tab - a run that is not for
|
|
# a tag builds and tests everything and publishes nothing.
|
|
#
|
|
# The rolling `latest` build this file used to keep is retired: there are
|
|
# versions now, and only versions.
|
|
#
|
|
# The shape mirrors how this is built by hand: the daemon first, then the app
|
|
# around it, with the Windows installer cross-compiled from Linux exactly as
|
|
# scripts/build-daemon-win.sh does locally. There is no Windows runner and
|
|
# there does not need to be one - the .exe CI ships is then the same kind of
|
|
# binary that has been shipped and tested from this desk all along.
|
|
#
|
|
# Every build step is an existing npm script rather than a reimplementation of
|
|
# one. scripts/build-daemon.sh and build-daemon-win.sh stay the single
|
|
# description of how the daemon gets built; if they change, this follows
|
|
# without being edited.
|
|
#
|
|
# Two properties this file is arranged to keep:
|
|
#
|
|
# - Nothing reaches a release unless typecheck and the Rust tests passed.
|
|
# That is structural rather than a convention: `publish` has `needs:` on
|
|
# both build jobs, and a needs-dependency on a failed job never runs.
|
|
# - What is published was built from a clean tree. See the "the tree is
|
|
# clean" step for why that is not automatic.
|
|
|
|
name: release
|
|
|
|
on:
|
|
push:
|
|
tags: ['v*']
|
|
workflow_dispatch:
|
|
|
|
# A hand-started check of a branch gives way to a newer one of the same
|
|
# branch. A tag is its own ref and is never cancelled, by a check or by
|
|
# another tag.
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: ${{ github.ref_type != 'tag' }}
|
|
|
|
# Read by default. Only `publish` asks for more, and only for itself.
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
# A release build does not use incremental compilation anyway; saying so
|
|
# keeps an incremental/ directory out of the cached target dir, where it
|
|
# would be carried back and forth forever and never hit.
|
|
CARGO_INCREMENTAL: 0
|
|
CARGO_TERM_COLOR: always
|
|
# crates.io from a hosted runner is not always well, and a connection reset
|
|
# forty minutes in is an expensive way to learn that.
|
|
CARGO_NET_RETRY: 10
|
|
RUSTUP_MAX_RETRIES: 10
|
|
|
|
jobs:
|
|
# --------------------------------------------------------------------
|
|
# Linux: the AppImage and the deb, and the job carrying the test gate.
|
|
#
|
|
# cargo test lives here rather than in a job of its own on purpose. A
|
|
# standalone gate would compile all 812 crates a third time, in the dev
|
|
# profile, sharing nothing with either build. nobilis has no
|
|
# [dev-dependencies] and no [profile] overrides, so run in the release
|
|
# profile straight after `npm run daemon` it reuses every rlib that build
|
|
# just produced and compiles only the crate's own test harness. That is the
|
|
# difference between minutes and another half hour - and it is what
|
|
# PKGBUILD's check() already does locally.
|
|
# --------------------------------------------------------------------
|
|
build-linux:
|
|
# Pinned to the oldest distribution moho supports, because the daemon is a
|
|
# native binary and glibc symbol versions only go forwards: one built
|
|
# against 2.39 refuses to start on anything older, whatever the AppImage
|
|
# around it can do. Built here on Arch that floor came out at 2.39 and on
|
|
# ubuntu-latest (24.04) it would be the same, while Ubuntu 22.04 has 2.35 -
|
|
# so the released daemon would not run on a distribution this project
|
|
# claims to support, and the failure would look like the app starting and
|
|
# the backend never coming up.
|
|
#
|
|
# Building on 22.04 sets the floor at 2.35, which every newer target
|
|
# satisfies: Debian 13 is on 2.41, Ubuntu 24.04 on 2.39. The Windows job
|
|
# needs none of this - it cross-builds with mingw and links no glibc at
|
|
# all. When this label is eventually retired, the replacement is a
|
|
# `container: ubuntu:22.04` on a current runner, not a bump to the next
|
|
# label along.
|
|
runs-on: ubuntu-22.04
|
|
timeout-minutes: 90
|
|
outputs:
|
|
# Published for the release body. It must not be recomputed there:
|
|
# git's --short length scales with the object count, so slicing the sha
|
|
# to a fixed width is a guess that eventually names a file that does not
|
|
# exist. This is the same string that named the artifacts.
|
|
build: ${{ steps.stamp.outputs.build }}
|
|
|
|
steps:
|
|
# nobilis is a submodule at a commit its own master holds, which is why
|
|
# this workflow only ever runs on master.
|
|
#
|
|
# Depth 1 is enough: nobilis/build.rs only asks git for a short sha and
|
|
# a dirty flag, and falls back to "unknown" rather than failing.
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
submodules: recursive
|
|
fetch-depth: 1
|
|
|
|
# libopus-dev is deliberately absent. Without it libopus_sys builds a
|
|
# static opus through CMake, which is what we want in something that has
|
|
# to run on a machine we know nothing about. libasound2-dev is not
|
|
# optional - cpal reaches alsa-sys, which wants it.
|
|
- name: system build dependencies
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y --no-install-recommends \
|
|
build-essential cmake pkg-config libasound2-dev xvfb
|
|
|
|
# The image ships a stable rust, but its version floats and this tree's
|
|
# effective floor is 1.93 (matrix-sdk-crypto). Ask explicitly.
|
|
#
|
|
# The version goes into the cache key below: artifacts compiled by
|
|
# another rustc cannot be reused, and a restored target dir full of them
|
|
# rebuilds everything while reporting a cache hit, which is a confusing
|
|
# way to lose twenty minutes.
|
|
- name: rust toolchain
|
|
run: |
|
|
rustup toolchain install stable --profile minimal --component clippy --no-self-update
|
|
rustup default stable
|
|
echo "RUSTC_VERSION=$(rustc -V | tr ' /' '--')" >> "$GITHUB_ENV"
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
cache: npm
|
|
|
|
# Registry index and downloaded crates only. registry/src is the
|
|
# extracted form of registry/cache and cargo recreates it on demand, so
|
|
# caching it doubles the archive for nothing.
|
|
- name: cache cargo registry
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
~/.cargo/registry/index
|
|
~/.cargo/registry/cache
|
|
~/.cargo/git/db
|
|
key: cargo-registry-${{ hashFiles('nobilis/Cargo.lock') }}
|
|
restore-keys: cargo-registry-
|
|
|
|
# The cache that matters. What costs half an hour here is compiling, not
|
|
# downloading: aws-lc and bundled sqlite go through CMake and cc, and the
|
|
# arti and matrix-sdk trees are large. A clean release build of this is
|
|
# about 1.7 GB of artifacts.
|
|
#
|
|
# The key deliberately excludes the commit sha. With the sha in it every
|
|
# push writes a fresh entry, and two jobs a few pushes a day evict the
|
|
# 10 GB repository budget within a day - leaving a cache that is never
|
|
# warm. Keyed on the lockfile it is written when dependencies change and
|
|
# hit on every push after that, so the only thing recompiled per push is
|
|
# nobilis itself, which is the few minutes that genuinely cannot be
|
|
# avoided.
|
|
#
|
|
# ImageOS is in the key because the runner image is part of what built
|
|
# these artifacts, and pinning this job to 22.04 changed it without
|
|
# changing anything the key looked at. The next run restored a tree
|
|
# compiled on 24.04 under an exact-match key, cargo believed the crates
|
|
# it named were already built, and the build died on "can't find crate
|
|
# for tor_bytes" - a dependency whose rlib the restored tree did not
|
|
# have. The Windows job, still on ubuntu-latest, restored its own
|
|
# matching cache and passed, which is how the image turned out to be
|
|
# the variable.
|
|
- name: cache the cargo target directory
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: nobilis/target
|
|
key: cargo-target-linux-${{ env.ImageOS }}-${{ hashFiles('nobilis/Cargo.lock') }}-${{ env.RUSTC_VERSION }}
|
|
# A lockfile bump should start from the previous tree and rebuild
|
|
# what actually changed, rather than starting from nothing. Scoped
|
|
# to the image for the reason above: falling back across images is
|
|
# the same trap one step further down.
|
|
restore-keys: cargo-target-linux-${{ env.ImageOS }}-
|
|
|
|
# The Electron binary, and the appimagetool and nsis bundles
|
|
# electron-builder fetches at pack time. Small, and it removes a real
|
|
# source of flakes: those are rate-limited downloads from GitHub.
|
|
- name: cache electron downloads
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
~/.cache/electron
|
|
~/.cache/electron-builder
|
|
key: electron-${{ hashFiles('package-lock.json') }}
|
|
restore-keys: electron-
|
|
|
|
- run: npm ci
|
|
|
|
# Before anything expensive. `npm run pack` builds the daemon first and
|
|
# typechecks afterwards, so without this a missing semicolon would not
|
|
# surface until half an hour in. The repeat inside pack costs seconds.
|
|
# A version tag that disagrees with the version in package.json would
|
|
# publish "v1.0.0" carrying an app that calls itself something else in
|
|
# Settings > About. Said before half an hour of building, not after.
|
|
- name: the tag matches the version
|
|
if: github.ref_type == 'tag'
|
|
run: |
|
|
want="$(node -p 'require("./package.json").version')"
|
|
if [ "$GITHUB_REF_NAME" != "v$want" ]; then
|
|
echo "::error::tag $GITHUB_REF_NAME, but package.json says $want - tag v$want, or bump the version first"
|
|
exit 1
|
|
fi
|
|
|
|
- name: typecheck
|
|
run: npm run typecheck
|
|
|
|
# The window's own unit tests: its formatting, its emote and media URL
|
|
# rewriting, the rail, search, mentions - and the media scheme's
|
|
# boundary. Seconds, so here beside the typecheck rather than after the
|
|
# long build.
|
|
- name: unit tests
|
|
run: npm test
|
|
|
|
# Two jobs in one line. It warms the registry, and --locked turns a
|
|
# stale lockfile into a loud failure here rather than a silent rewrite
|
|
# later: the npm scripts call plain `cargo build`, which would quietly
|
|
# update Cargo.lock, dirty the working tree, and name the asset
|
|
# moho-<sha>-dirty out of a pristine checkout.
|
|
- name: verify the lockfile, and fetch what it names
|
|
run: cargo fetch --locked --manifest-path nobilis/Cargo.toml
|
|
|
|
- name: build the daemon
|
|
run: npm run daemon
|
|
|
|
# The runner pin above is load-bearing and invisible: nothing about a
|
|
# daemon built on too new a distribution looks wrong until somebody on
|
|
# an older one finds the backend never starts. So it is asserted rather
|
|
# than trusted, here where the answer is cheap and the failure names
|
|
# itself.
|
|
- name: the daemon runs on the oldest target
|
|
run: |
|
|
floor="$(objdump -T nobilis/target/release/nobilis \
|
|
| grep -o 'GLIBC_[0-9.]*' | sort -uV | tail -1)"
|
|
echo "highest glibc symbol required: $floor"
|
|
highest="$(printf '%s\n' "${floor#GLIBC_}" 2.35 | sort -V | tail -1)"
|
|
if [ "$highest" != "2.35" ]; then
|
|
echo "::error::the daemon needs $floor, which Ubuntu 22.04 (2.35) does not have"
|
|
exit 1
|
|
fi
|
|
|
|
# Reuses the release artifacts built one step above. The 19 tests that
|
|
# want the network, Tor, a sound device or real credentials are all
|
|
# #[ignore]d, so this is hermetic on a runner.
|
|
- name: cargo test
|
|
run: cargo test --release --locked --manifest-path nobilis/Cargo.toml
|
|
|
|
# Held at zero, so a lint is the change that introduced it rather than
|
|
# one more in a pile nobody reads. Release profile for the same reason
|
|
# as the tests: the dependencies were checked by the build above.
|
|
- name: cargo clippy
|
|
run: cargo clippy --release --locked --all-targets --manifest-path nobilis/Cargo.toml -- -D warnings
|
|
|
|
# Layout, which only a real browser can check: the built app is run
|
|
# against a scratch home and a fake IRC server, every popup is opened at
|
|
# two window sizes, and each must stay inside the window - measured at
|
|
# several moments, because a popup that grows after it opens is the bug
|
|
# this was written for. Needs the daemon built above.
|
|
- name: layout check
|
|
run: xvfb-run -a npm run test:ui
|
|
|
|
# The tray cannot be looked at, so this reads what main gave it - icon,
|
|
# tooltip, menu - and presses its entries, including Restart moho, which
|
|
# has to bring back both the window and a daemon of its own.
|
|
- name: tray check
|
|
run: xvfb-run -a npm run test:tray
|
|
|
|
# GIFs: that a message carrying one is drawn as the picture, that a page
|
|
# nothing could read stays a link, and that the picker finds and sends.
|
|
# Needs ffmpeg, for the pictures it plays.
|
|
- name: gif check
|
|
run: |
|
|
sudo apt-get install -y --no-install-recommends ffmpeg
|
|
xvfb-run -a npm run test:gif
|
|
|
|
# Scrolling through a long unread run: that the view stays where it is
|
|
# read when something above it changes size, with the "new messages" bar
|
|
# up over the top of the log.
|
|
- name: scroll check
|
|
run: xvfb-run -a npm run test:scroll
|
|
|
|
# Kick event cards: where they sit in each layout, measured against a
|
|
# chat line beside them - the picture's column, the name's place, and the
|
|
# air above.
|
|
- name: kick card check
|
|
run: xvfb-run -a npm run test:kick
|
|
|
|
# MOHO_BUILD is computed inside the npm script - a shell assignment on
|
|
# the electron-builder line - so it cannot be injected from out here.
|
|
# The only way to control what the artifact is called is to guarantee
|
|
# the tree it is named after is clean.
|
|
#
|
|
# Failing is the point. A -dirty suffix on a public release built from a
|
|
# clean checkout is a lie about provenance, and it would also put the
|
|
# file's name out of step with the commit the release body claims.
|
|
- name: the tree is clean
|
|
id: stamp
|
|
run: |
|
|
if ! git diff --quiet HEAD; then
|
|
echo "::error::the checkout was modified during the build - the artifact would be named -dirty"
|
|
git status --porcelain
|
|
exit 1
|
|
fi
|
|
echo "build=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
|
|
|
|
# `npm run daemon` inside pack is a no-op against the tree just built.
|
|
# --publish never because there is no publish configuration and no token
|
|
# in this job - saying so means a later package.json change cannot
|
|
# quietly turn a build job into a publishing one.
|
|
- name: pack the AppImage and the deb
|
|
run: npm run pack:linux -- --publish never
|
|
|
|
# Here to be read on the first few runs: it says how much headroom there
|
|
# actually was, rather than how much was estimated.
|
|
- run: df -h /
|
|
if: always()
|
|
|
|
# The two packages only. electron-builder also emits a .blockmap, a
|
|
# latest-linux.yml for its own updater and a builder-debug.yml, none of
|
|
# which belong on the release - so this names what to take rather than
|
|
# sweeping dist/.
|
|
- uses: actions/upload-artifact@v4
|
|
with:
|
|
name: linux
|
|
path: |
|
|
dist/*.AppImage
|
|
dist/*.deb
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
|
|
# The daemon on its own, for nobilis's release - attached to that by
|
|
# scripts/release-nobilis.sh, since this job's token can only release
|
|
# here. Not an asset of moho's release: the AppImage already carries it.
|
|
- uses: actions/upload-artifact@v4
|
|
if: github.ref_type == 'tag'
|
|
with:
|
|
name: daemon-linux
|
|
path: nobilis/target/release/nobilis
|
|
if-no-files-found: error
|
|
retention-days: 30
|
|
|
|
# --------------------------------------------------------------------
|
|
# Windows, cross-compiled. Same runner OS, same scripts, plus mingw.
|
|
#
|
|
# In parallel with the Linux job rather than after it, so a run takes as
|
|
# long as the slower of the two rather than the sum. The trade is that a
|
|
# failing test does not stop this job finishing: wasted compute, never a
|
|
# wasted publish.
|
|
# --------------------------------------------------------------------
|
|
build-windows:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 90
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
submodules: recursive
|
|
fetch-depth: 1
|
|
|
|
# No pkg-config and no libasound2-dev here, deliberately: the cross
|
|
# build must not find the host's Linux libraries. In particular do NOT
|
|
# set PKG_CONFIG_ALLOW_CROSS - libopus_sys' pkg-config probe correctly
|
|
# fails on the cross target and falls back to building opus from its
|
|
# vendored C source, and allowing cross would let it link the host's
|
|
# Linux libopus instead. That produces an .exe that fails on somebody
|
|
# else's machine rather than here, which is the worst place to find out.
|
|
# wine is here because electron-builder needs it even though nothing is
|
|
# signed. Packaging for Windows runs the executable through signtool,
|
|
# which is a Windows program, and it is that call - not the signing,
|
|
# which it then skips for want of a certificate - that wants wine. The
|
|
# first run died at exactly this step with "wine is required".
|
|
#
|
|
# The alternative is win.signAndEditExecutable: false, which skips the
|
|
# call and with it the icon and version metadata the installer shows.
|
|
# Paying a minute of apt for an .exe that looks like the one we ship by
|
|
# hand is the better trade.
|
|
#
|
|
# Asked for by both names because the package split differs between
|
|
# Ubuntu releases, then checked: an install that quietly put no `wine`
|
|
# on PATH would fail thirty minutes later at the pack step instead of
|
|
# here.
|
|
- name: system build dependencies
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y --no-install-recommends \
|
|
build-essential cmake gcc-mingw-w64-x86-64
|
|
# i386 as well as amd64, because the program wine is needed for is
|
|
# a 32-bit one: electron-builder writes the icon and version
|
|
# metadata into the .exe with rcedit-ia32.exe, and a 64-bit-only
|
|
# wine dies loading it on a missing syswow64 ntdll.
|
|
sudo dpkg --add-architecture i386
|
|
sudo apt-get update
|
|
# Recommends left ON here, unlike everything above. The `wine`
|
|
# package is a thin thing whose 32-bit half arrives as a recommend;
|
|
# asking for wine64 and wine32 directly instead installs
|
|
# /usr/lib/wine/wine64 and nothing in /usr/bin at all, which is a
|
|
# wine that no caller can find.
|
|
sudo apt-get install -y wine
|
|
# Ubuntu names the binary wine-stable and hangs /usr/bin/wine off it
|
|
# through alternatives, which may or may not have been set up.
|
|
# electron-builder looks for `wine`, so make sure that name exists
|
|
# whichever way round this image has it.
|
|
if ! command -v wine >/dev/null; then
|
|
target="$(command -v wine-stable || true)"
|
|
[ -n "$target" ] || { echo "::error::wine installed but no binary is on PATH"; exit 1; }
|
|
sudo ln -s "$target" /usr/local/bin/wine
|
|
fi
|
|
wine --version
|
|
# And prove the 32-bit half is really there rather than trusting
|
|
# apt, by building the prefix and looking for the very file whose
|
|
# absence failed the pack step. `wine --version` passed on a wine
|
|
# that could not run rcedit at all, so on its own it is not a check
|
|
# worth having.
|
|
WINEDEBUG=-all timeout 300 wine wineboot --init >/dev/null 2>&1 || true
|
|
test -f "$HOME/.wine/drive_c/windows/syswow64/ntdll.dll" \
|
|
|| { echo "::error::wine has no 32-bit support - rcedit cannot run"; exit 1; }
|
|
|
|
- name: rust toolchain
|
|
run: |
|
|
rustup toolchain install stable --profile minimal --no-self-update
|
|
rustup default stable
|
|
rustup target add x86_64-pc-windows-gnu
|
|
echo "RUSTC_VERSION=$(rustc -V | tr ' /' '--')" >> "$GITHUB_ENV"
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
cache: npm
|
|
|
|
- name: cache cargo registry
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
~/.cargo/registry/index
|
|
~/.cargo/registry/cache
|
|
~/.cargo/git/db
|
|
key: cargo-registry-${{ hashFiles('nobilis/Cargo.lock') }}
|
|
restore-keys: cargo-registry-
|
|
|
|
# Its own key, not shared with the Linux job. The same directory holds
|
|
# different things here: host artifacts for build scripts and proc
|
|
# macros under target/release/, and the real output under
|
|
# target/x86_64-pc-windows-gnu/. One key for both would be two jobs
|
|
# fighting over one entry and neither getting a hit.
|
|
- name: cache the cargo target directory
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: nobilis/target
|
|
# Scoped to the image for the same reason as the Linux job's: this
|
|
# directory also holds host-built artifacts - build scripts and
|
|
# proc macros - even though the binary itself is cross-compiled.
|
|
key: cargo-target-windows-gnu-${{ env.ImageOS }}-${{ hashFiles('nobilis/Cargo.lock') }}-${{ env.RUSTC_VERSION }}
|
|
restore-keys: cargo-target-windows-gnu-${{ env.ImageOS }}-
|
|
|
|
- name: cache electron downloads
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
~/.cache/electron
|
|
~/.cache/electron-builder
|
|
key: electron-${{ hashFiles('package-lock.json') }}
|
|
restore-keys: electron-
|
|
|
|
- run: npm ci
|
|
|
|
- name: typecheck
|
|
run: npm run typecheck
|
|
|
|
- name: verify the lockfile, and fetch what it names
|
|
run: cargo fetch --locked --manifest-path nobilis/Cargo.toml
|
|
|
|
# build-daemon-win.sh builds for the gnu target and copies the .exe to
|
|
# nobilis/target/release/, which is where extraResources looks for it.
|
|
# It checks that file exists rather than trusting the exit code -
|
|
# against a restored target dir that is exactly the stale-binary case it
|
|
# was written for.
|
|
- name: build the Windows daemon
|
|
run: npm run daemon:win
|
|
|
|
- name: the tree is clean
|
|
run: |
|
|
if ! git diff --quiet HEAD; then
|
|
echo "::error::the checkout was modified during the build - the artifact would be named -dirty"
|
|
git status --porcelain
|
|
exit 1
|
|
fi
|
|
|
|
- name: pack the installer
|
|
run: npm run pack:win -- --publish never
|
|
|
|
- run: df -h /
|
|
if: always()
|
|
|
|
- uses: actions/upload-artifact@v4
|
|
with:
|
|
name: windows
|
|
path: dist/*.exe
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
|
|
- uses: actions/upload-artifact@v4
|
|
if: github.ref_type == 'tag'
|
|
with:
|
|
name: daemon-windows
|
|
path: nobilis/target/release/nobilis.exe
|
|
if-no-files-found: error
|
|
retention-days: 30
|
|
|
|
# --------------------------------------------------------------------
|
|
# A version tag only: the release, its files and its changelog.
|
|
#
|
|
# The notes are the version's section of CHANGELOG.md - major features,
|
|
# then fixes, patches and changes, each linked to its commit - followed by
|
|
# the downloads table. A version with no section is not published: a
|
|
# release without its changelog is the one thing this step exists to stop.
|
|
# --------------------------------------------------------------------
|
|
publish:
|
|
if: github.ref_type == 'tag'
|
|
needs: [build-linux, build-windows]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
permissions:
|
|
contents: write
|
|
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
GH_REPO: ${{ github.repository }}
|
|
BUILD: ${{ needs.build-linux.outputs.build }}
|
|
|
|
steps:
|
|
# Only the changelog is needed from the tree.
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
sparse-checkout: CHANGELOG.md
|
|
sparse-checkout-cone-mode: false
|
|
|
|
- uses: actions/download-artifact@v4
|
|
with:
|
|
pattern: '{linux,windows}'
|
|
path: artifacts
|
|
merge-multiple: true
|
|
|
|
# Three files, no more and no fewer: AppImage, deb, Windows installer.
|
|
# Built named after the commit (the name is decided inside the npm
|
|
# script); published named after the version.
|
|
- name: name the files after the version
|
|
run: |
|
|
version="${GITHUB_REF_NAME#v}"
|
|
test "$(find artifacts -maxdepth 1 -type f | wc -l)" -eq 3
|
|
for f in artifacts/moho-"$BUILD".*; do
|
|
mv -v "$f" "artifacts/moho-$version.${f##*.}"
|
|
done
|
|
ls -l artifacts
|
|
|
|
- name: compose the notes from the changelog
|
|
run: |
|
|
version="${GITHUB_REF_NAME#v}"
|
|
# The section headed "## [<version>]", up to the next "## [".
|
|
section() {
|
|
awk -v v="$1" '
|
|
index($0, "## [" v "]") == 1 { on = 1; next }
|
|
on && /^## \[/ { exit }
|
|
on { print }
|
|
' CHANGELOG.md
|
|
}
|
|
section "$version" > "$RUNNER_TEMP/changes.md"
|
|
# A release candidate rehearses its release's notes, unless it has
|
|
# a section of its own: v1.0.0-rc.1 publishes "## [1.0.0]".
|
|
if ! grep -q '[^[:space:]]' "$RUNNER_TEMP/changes.md" && [ "${version%%-*}" != "$version" ]; then
|
|
section "${version%%-*}" > "$RUNNER_TEMP/changes.md"
|
|
fi
|
|
if ! grep -q '[^[:space:]]' "$RUNNER_TEMP/changes.md"; then
|
|
echo "::error::CHANGELOG.md has no section for $version - write it, then tag again"
|
|
exit 1
|
|
fi
|
|
{
|
|
cat "$RUNNER_TEMP/changes.md"
|
|
echo
|
|
echo "## Downloads"
|
|
echo
|
|
echo "| File | Platform |"
|
|
echo "| --- | --- |"
|
|
echo "| \`moho-$version.AppImage\` | Linux x86_64 - mark it executable and run it |"
|
|
echo "| \`moho-$version.deb\` | Debian / Ubuntu x86_64 |"
|
|
echo "| \`moho-$version.exe\` | Windows x64 - a per-user installer, no administrator needed |"
|
|
echo
|
|
echo "**The Windows installer is not code-signed.** SmartScreen warns when you run it:"
|
|
echo "choose *More info*, then *Run anyway*. If Windows 11's **Smart App Control** is on,"
|
|
echo "moho installs but never opens; turn it off in *Windows Security > App & browser"
|
|
echo "control > Smart App Control settings*. The installer checks and warns."
|
|
} > "$RUNNER_TEMP/notes.md"
|
|
cat "$RUNNER_TEMP/notes.md"
|
|
|
|
# Created as a draft and published once its files are attached, so
|
|
# nobody lands on a version with nothing to download. Always a full
|
|
# release and GitHub's "Latest", a release candidate included - shown on
|
|
# the repository's page, not tucked away as a pre-release. A tag pushed
|
|
# again finds its release there: the files are replaced, the notes too,
|
|
# since the changelog is where they are corrected.
|
|
- name: publish the version
|
|
run: |
|
|
version="${GITHUB_REF_NAME#v}"
|
|
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
|
|
gh release edit "$GITHUB_REF_NAME" --notes-file "$RUNNER_TEMP/notes.md"
|
|
gh release upload "$GITHUB_REF_NAME" artifacts/* --clobber
|
|
else
|
|
gh release create "$GITHUB_REF_NAME" --draft --verify-tag \
|
|
--title "moho $version" --notes-file "$RUNNER_TEMP/notes.md"
|
|
gh release upload "$GITHUB_REF_NAME" artifacts/*
|
|
gh release edit "$GITHUB_REF_NAME" --draft=false --latest
|
|
fi
|