Skip to content

Release

Release #136

Workflow file for this run

# AsyncAO release pipeline. Pushing a tag like `v0.1.0` builds every platform
# (full default + a lean `-tags "nodiscord novoice"` flavour with Discord Rich
# Presence AND the LemmyAO/Nyathena voice chat compiled out), STAMPS the running
# version into the binary (so the in-app self-update check works — an unstamped
# build reports "dev" and never updates), and publishes a GitHub Release with all
# the assets attached. This is the "use the GitHub API instead of shipping to
# testers by hand" pipeline: tag, push, done.
#
# Asset names are deliberate — the self-updater downloads ONE asset and renames
# it straight over the running executable, so the swappable default MUST be a bare
# binary whose name carries this platform's token (see update.SelfUpdateAssetMatch):
# asyncao-windows-x86_64.exe <- Windows self-update target (DLLs already beside it)
# asyncao-windows-x86_64-bundle.zip <- Windows FIRST install (exe + runtime DLLs)
# AsyncAO-linux-x86_64.AppImage <- Linux: self-contained, serves install AND self-update
# asyncao-macos-arm64 <- macOS (Apple Silicon) self-update target (rewritten, loads ./lib)
# asyncao-macos-bundle-arm64.tar.gz <- macOS FIRST install (binary + bundled lib/, self-contained)
# asyncao-macos-homebrew-arm64.tar.gz <- macOS Homebrew edition (bare binary + INSTALL.txt, brew supplies libs)
# The -nodiscord / -bundle / -homebrew variants are named so they never collide
# with a token. Token-dodge for the macOS bundle AND homebrew tarballs: the darwin
# self-update token is the substring "macos-arm64"; neither "macos-bundle-arm64"
# nor "macos-homebrew-arm64" contains it (the "-bundle-"/"-homebrew-" breaks the
# run), so the self-updater never renames a .tar.gz over the running binary —
# same dodge as the -nodiscord- and Windows -bundle.zip names.
name: Release
on:
push:
tags: ["v*"]
# Manual re-run for debugging the pipeline without cutting a tag (publishes
# nothing unless a matching tag exists — gh --verify-tag guards that).
workflow_dispatch:
permissions:
contents: write # gh release create writes a Release on this repo
env:
GO_VERSION: "1.24"
# The version stamped into every binary for the self-update check (inlined into
# each build step as `…Version=${{ github.ref_name }}`). github.ref_name is the
# tag (e.g. v0.1.0) on a tag push; a manual dispatch from a branch stamps the
# branch name, which compareSemver reads as 0.0.0 (harmless — that run won't
# publish anyway, see the release job's tag guard).
jobs:
build-windows:
name: Windows x86_64${{ matrix.variant.label }}
runs-on: windows-latest
strategy:
matrix:
variant:
- { id: default, label: "", tags: "", exe: "asyncao-windows-x86_64.exe", zip: "asyncao-windows-x86_64-bundle.zip" }
- { id: nodiscord, label: " (Discord-free, no voice)", tags: "nodiscord novoice", exe: "asyncao-windows-x86_64-nodiscord.exe", zip: "asyncao-windows-x86_64-nodiscord-bundle.zip" }
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v4
- uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
update: true
install: >-
mingw-w64-ucrt-x86_64-gcc
mingw-w64-ucrt-x86_64-pkgconf
mingw-w64-ucrt-x86_64-SDL2
mingw-w64-ucrt-x86_64-SDL2_ttf
mingw-w64-ucrt-x86_64-SDL2_mixer
mingw-w64-ucrt-x86_64-libwebp
mingw-w64-ucrt-x86_64-libavif
mingw-w64-ucrt-x86_64-opus
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
# See ci.yml: setup-go's PATH edit is invisible to the MSYS2 shell, so
# republish GOROOT (a non-PATH env var DOES cross into MSYS2).
- name: Expose GOROOT to the MSYS2 shell
shell: pwsh
run: '"GOROOT=$(go env GOROOT)" | Out-File -FilePath $env:GITHUB_ENV -Append'
# Regenerate the Windows VERSIONINFO syso with THIS tag's version so the
# .exe's Properties → Details shows the real release version, not the
# committed 1.62.0.0 baseline. BEST-EFFORT: continue-on-error + a guarded
# script, so any failure (network, tool, a non-semver dispatch ref) falls
# back silently to the committed cmd/asyncao/versioninfo_windows.syso — the
# build never depends on this succeeding. goversioninfo is PINNED (never
# @latest) and run via `go run` so it never enters go.mod. -64 emits an
# amd64 COFF the amd64 link can consume (a 32-bit syso would fail to link).
- name: Regenerate VERSIONINFO with the tag version (best-effort)
continue-on-error: true
run: |
export PATH="$(cygpath -u "$GOROOT")/bin:$PATH"
ver="${{ github.ref_name }}"
ver="${ver#v}" # strip a leading v
ver="${ver%%-*}" # drop any prerelease suffix (-test.N)
IFS='.' read -r MA MI PA _ <<< "$ver"
# Only proceed when the tag parsed into numeric major.minor.patch;
# otherwise leave the committed syso in place.
if [[ "$MA" =~ ^[0-9]+$ && "$MI" =~ ^[0-9]+$ && "$PA" =~ ^[0-9]+$ ]]; then
j=cmd/asyncao/versioninfo.json
gen=cmd/asyncao/_versioninfo.tag.json
# Rewrite the four numeric FixedFileInfo fields and both dotted
# StringFileInfo version strings to this tag; leave every other field.
sed -e "s/\"Major\": [0-9]*/\"Major\": $MA/g" \
-e "s/\"Minor\": [0-9]*/\"Minor\": $MI/g" \
-e "s/\"Patch\": [0-9]*/\"Patch\": $PA/g" \
-e "s/\"FileVersion\": \"[0-9.]*\"/\"FileVersion\": \"$MA.$MI.$PA.0\"/g" \
-e "s/\"ProductVersion\": \"[0-9.]*\"/\"ProductVersion\": \"$MA.$MI.$PA.0\"/g" \
"$j" > "$gen"
go run github.com/josephspurrier/goversioninfo/cmd/goversioninfo@v1.4.0 \
-64 -o cmd/asyncao/versioninfo_windows.syso "$gen"
rm -f "$gen"
echo "Regenerated VERSIONINFO for $MA.$MI.$PA.0"
else
echo "::notice::Non-numeric ref ${{ github.ref_name }} - keeping the committed VERSIONINFO syso."
fi
- name: Build + bundle DLLs
run: |
export PATH="$(cygpath -u "$GOROOT")/bin:$PATH"
export CGO_ENABLED=1
mkdir -p dist
# -H=windowsgui: GUI subsystem, so the released exe opens no console window.
go build -tags "${{ matrix.variant.tags }}" -pgo=auto -trimpath \
-ldflags "-s -w -H=windowsgui -X github.com/SyntaxNyah/AsyncAO/internal/update.Version=${{ github.ref_name }}" \
-o dist/asyncao.exe ./cmd/asyncao
# Ship the runtime DLLs next to the exe so the bundle runs anywhere.
ldd dist/asyncao.exe | grep -io '/ucrt64/bin/[a-z0-9_.+-]*\.dll' | sort -u | while read -r dll; do
cp "$dll" dist/
done
# Authenticode signing — reduces the SmartScreen "unknown publisher" warning
# (an OV cert builds reputation over time; an EV cert clears it immediately).
# GATED: with no WINDOWS_CODESIGN_PFX_BASE64 secret it skips cleanly and ships
# an UNSIGNED build, so the pipeline works before the user has a certificate.
# Runs BEFORE zipping so both the bare exe AND the bundle carry the signature.
# See docs/CODE-SIGNING.md for the secrets to add.
- name: Sign the Windows binary (skipped without a cert secret)
shell: pwsh
env:
WIN_CERT_BASE64: ${{ secrets.WINDOWS_CODESIGN_PFX_BASE64 }}
WIN_CERT_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
run: |
if ([string]::IsNullOrEmpty($env:WIN_CERT_BASE64)) {
Write-Host "::notice::No WINDOWS_CODESIGN_PFX_BASE64 secret - shipping an UNSIGNED Windows build (SmartScreen may warn). See docs/CODE-SIGNING.md."
exit 0
}
$pfx = Join-Path $env:RUNNER_TEMP "asyncao-codesign.pfx"
[IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:WIN_CERT_BASE64))
$signtool = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin\*\x64\signtool.exe" -ErrorAction SilentlyContinue |
Sort-Object FullName -Descending | Select-Object -First 1
if (-not $signtool) { Write-Error "signtool.exe not found in the Windows SDK"; exit 1 }
# SHA-256 digest + an RFC-3161 timestamp so the signature stays valid after the cert expires.
& $signtool.FullName sign /f $pfx /p $env:WIN_CERT_PASSWORD /fd SHA256 `
/tr http://timestamp.digicert.com /td SHA256 dist\asyncao.exe
if ($LASTEXITCODE -ne 0) { Write-Error "signtool failed ($LASTEXITCODE)"; exit 1 }
& $signtool.FullName verify /pa dist\asyncao.exe
Remove-Item $pfx -Force
Write-Host "Signed dist\asyncao.exe"
- name: Name the release assets (bare exe + DLL bundle)
shell: pwsh
run: |
Copy-Item dist\asyncao.exe ".\${{ matrix.variant.exe }}"
Compress-Archive -Path dist\* -DestinationPath ".\${{ matrix.variant.zip }}" -Force
- uses: actions/upload-artifact@v4
with:
name: release-windows-${{ matrix.variant.id }}
path: |
${{ matrix.variant.exe }}
${{ matrix.variant.zip }}
build-linux:
name: Linux x86_64 AppImage${{ matrix.variant.label }}
runs-on: ubuntu-latest
strategy:
matrix:
variant:
- { id: default, label: "", tags: "", out: "AsyncAO-linux-x86_64.AppImage" }
- { id: nodiscord, label: " (Discord-free, no voice)", tags: "nodiscord novoice", out: "AsyncAO-linux-x86_64-nodiscord.AppImage" }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Install SDL2 + libwebp + libavif + AppImage tooling
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev libwebp-dev libavif-dev libopus-dev \
patchelf desktop-file-utils
- name: Build (version-stamped)
run: |
export CGO_ENABLED=1
go build -tags "${{ matrix.variant.tags }}" -pgo=auto -trimpath \
-ldflags "-s -w -X github.com/SyntaxNyah/AsyncAO/internal/update.Version=${{ github.ref_name }}" \
-o asyncao ./cmd/asyncao
- name: Package the AppImage
run: |
chmod +x scripts/build-appimage.sh
APPIMAGE_OUTPUT="${{ matrix.variant.out }}" scripts/build-appimage.sh ./asyncao
- uses: actions/upload-artifact@v4
with:
name: release-linux-${{ matrix.variant.id }}
path: dist/${{ matrix.variant.out }}
build-macos:
name: macOS arm64${{ matrix.variant.label }}
runs-on: macos-latest
strategy:
matrix:
# `asset` = the bare, self-contained self-update binary (its name carries
# the darwin "macos-arm64" token; the self-updater matches it).
# `bundle` = the FIRST-install tarball (binary + bundled lib/). Its name is
# "...-macos-bundle-arm64.tar.gz": it must NOT contain the
# "macos-arm64" substring, or the self-updater would rename the
# .tar.gz over the running binary and brick the install. The
# "-bundle-" between "macos" and "arm64" breaks the substring —
# NEVER rename this to "...-macos-arm64-bundle...".
#
# `homebrew` = the SMALL Homebrew edition (bare binary + INSTALL.txt, NO
# bundled lib/): the user runs `brew install …` and the binary
# resolves its libs via the /opt/homebrew/lib rpath fallback
# bundle-macos.sh added. Its name "...-macos-homebrew-arm64.tar.gz"
# must NOT contain "macos-arm64" for the same token reason as the
# bundle — "-homebrew-" breaks the run. NEVER rename it to put
# "arm64" straight after "macos".
variant:
- { id: default, label: "", tags: "", asset: "asyncao-macos-arm64", bundle: "asyncao-macos-bundle-arm64.tar.gz", homebrew: "asyncao-macos-homebrew-arm64.tar.gz" }
- { id: nodiscord, label: " (Discord-free, no voice)", tags: "nodiscord novoice", asset: "asyncao-macos-nodiscord-arm64", bundle: "asyncao-macos-nodiscord-bundle-arm64.tar.gz", homebrew: "asyncao-macos-homebrew-nodiscord-arm64.tar.gz" }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
# dylibbundler collects the non-system dylib closure beside the binary — the
# macOS analogue of build.ps1's DLL staging (see scripts/bundle-macos.sh).
# opusfile (not the bare `opus` codec, which is voice-chat-only) is what
# gives SDL2_mixer Ogg-Opus music decode/seek; brew pulls it anyway as an
# sdl2_mixer dependency, but pin it explicitly (ci.yml installs the same
# list — keep them consistent).
- name: Install SDL2 + libwebp + libavif
run: brew install sdl2 sdl2_ttf sdl2_mixer webp libavif opus opusfile pkg-config dylibbundler
- name: Build (version-stamped)
run: |
export CGO_ENABLED=1
go build -tags "${{ matrix.variant.tags }}" -pgo=auto -trimpath \
-ldflags "-s -w -X github.com/SyntaxNyah/AsyncAO/internal/update.Version=${{ github.ref_name }}" \
-o "${{ matrix.variant.asset }}" ./cmd/asyncao
# Make the binary self-contained: bundle its dylib closure into a staging
# folder with @rpath install names, and rewrite the bare `asset` in place so
# it too loads ./lib. This runs BEFORE codesign so the proper Developer-ID
# signature (when secrets exist) is the LAST thing applied. The script also
# ad-hoc signs, so the no-secret path still ships runnable arm64 code.
#
# FAIL-OPEN, like every other fragile mac step (codesign below, Flatpak, the
# Windows VERSIONINFO regen): a bundler/otool-guard hiccup on macOS must NOT
# strand the Windows + Linux release (continue-on-error keeps the job green,
# so the release job's needs.build-macos success gate still passes). But
# fail-open must not become fail-DIRTY: the bare asset already exists from
# the build step still Homebrew-linked, and bundle-macos.sh creates the
# staging dir before its fallible steps — so on ANY bundler/guard failure we
# DELETE both. Shipping that bare asset would hand every tarball-install
# self-updater a binary dyld can't launch without Homebrew (the updater's
# preflight now refuses Homebrew-linked binaries, but the asset must not
# exist in the first place), and tar would happily pack the partial stage.
# With both gone, upload-artifact finds nothing (if-no-files-found default
# `warn`), the release ships without this variant's mac assets, and the
# in-app updater falls back to opening the release page. The same blocking
# assertion runs in ci.yml (identical brew set + script) on every branch
# push; this cleanup covers formula drift between that CI run and tag time.
- name: Bundle dylibs (self-contained)
continue-on-error: true
env:
ASSET: ${{ matrix.variant.asset }}
run: |
if ! bash scripts/bundle-macos.sh "$ASSET" "macos-stage"; then
echo "::warning::bundle-macos.sh failed — deleting the $ASSET assets so nothing Homebrew-linked can ship"
rm -f "$ASSET"
rm -rf macos-stage
exit 1
fi
# Developer ID codesign + notarize. GATED on the Apple secrets and entirely
# BEST-EFFORT: with no cert it skips cleanly, and a signing/notary hiccup logs a
# warning but never fails the job (an unsigned-but-built binary still ships) — the
# same non-blocking stance as the Flatpak job, since this can't be tested here.
# See docs/CODE-SIGNING.md for the secrets and how to obtain the certificate.
- name: Codesign + notarize (skipped without Apple secrets)
continue-on-error: true
env:
APPLE_P12_BASE64: ${{ secrets.APPLE_CODESIGN_P12_BASE64 }}
APPLE_P12_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }}
APPLE_IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_NOTARY_APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_NOTARY_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_NOTARY_TEAM_ID }}
ASSET: ${{ matrix.variant.asset }}
run: |
# A failed bundle step deleted the asset + stage: skip deliberately
# (quiet, accurate log) instead of dying mid-keychain-setup on a
# missing path — and never print the "shipping" notice for a variant
# that ships nothing.
if [ ! -f "macos-stage/$ASSET" ]; then
echo "::warning::no staged bundle (bundler failed earlier) — skipping codesign"
exit 1
fi
if [ -z "$APPLE_P12_BASE64" ]; then
# No cert: bundle-macos.sh already ad-hoc signed the binary AND every
# lib/*.dylib, so the staged folder runs on a clean Mac (Gatekeeper
# warns, nothing breaks). Nothing to re-sign here.
echo "::notice::No APPLE_CODESIGN_P12_BASE64 secret - shipping an ad-hoc-signed (unnotarized) macOS build (Gatekeeper will warn). See docs/CODE-SIGNING.md."
exit 0
fi
set -x
KEYCHAIN="$RUNNER_TEMP/asyncao.keychain-db"
KEYCHAIN_PW="$(uuidgen)"
security create-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN"
echo "$APPLE_P12_BASE64" | base64 --decode > "$RUNNER_TEMP/cert.p12"
security import "$RUNNER_TEMP/cert.p12" -k "$KEYCHAIN" -P "$APPLE_P12_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PW" "$KEYCHAIN"
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"')
# Sign INSIDE-OUT: notarization requires every nested Mach-O (the bundled
# dylibs) to be signed before the code that loads them. --force replaces
# the ad-hoc signature bundle-macos.sh applied. The bare self-update
# `$ASSET` and the staged copy are the same file content but separate
# paths, so sign both.
STAGED="macos-stage/$ASSET"
for dylib in macos-stage/lib/*.dylib; do
[ -e "$dylib" ] || continue
codesign --force --options runtime --timestamp --sign "$APPLE_IDENTITY" "$dylib"
done
codesign --force --options runtime --timestamp --sign "$APPLE_IDENTITY" "$STAGED"
codesign --verify --strict --verbose=2 "$STAGED"
# The bare self-update asset ships without lib/, so sign it standalone too.
codesign --force --options runtime --timestamp --sign "$APPLE_IDENTITY" "$ASSET"
codesign --verify --strict --verbose=2 "$ASSET"
# Notarize the WHOLE staged folder (binary + lib/), not just the bare
# binary: Gatekeeper checks the nested dylibs too. A folder/binary can't
# be stapled (needs an .app/.dmg/.pkg), so verification is online on
# first run — zip the staged dir with its lib/ intact.
if [ -n "$APPLE_ID" ] && [ -n "$APPLE_ID_PASSWORD" ] && [ -n "$APPLE_TEAM_ID" ]; then
ditto -c -k --keepParent "macos-stage" "$RUNNER_TEMP/notarize.zip"
xcrun notarytool submit "$RUNNER_TEMP/notarize.zip" \
--apple-id "$APPLE_ID" --password "$APPLE_ID_PASSWORD" --team-id "$APPLE_TEAM_ID" --wait
else
echo "::notice::Codesigned but NOT notarized (no APPLE_NOTARY_* secrets)."
fi
security delete-keychain "$KEYCHAIN"
# Pack the first-install tarball: the staged folder (binary + lib/) that
# runs on a clean Mac. --keepParent isn't used; we cd into the stage so the
# archive holds the binary + lib/ at the top level (unzip-and-run).
# FAIL-OPEN for the same reason as the bundle step above: a mac packaging
# hiccup omits the mac assets (upload-artifact warns on the absent file) but
# never blocks the Windows + Linux release. The explicit stage-dir guard
# (not just letting tar error) makes the skip-on-failed-bundle deliberate:
# the bundle step deleted the stage, so there is nothing safe to pack.
- name: Pack first-install tarball
continue-on-error: true
env:
ASSET: ${{ matrix.variant.asset }}
run: |
if [ ! -f "macos-stage/$ASSET" ]; then
echo "::warning::no staged bundle (bundler failed earlier) — skipping the tarball"
exit 1
fi
# Drop the bundle-edition install guide INTO the tarball (packed by the
# `-C macos-stage .` below). It surfaces the Gatekeeper quarantine relief
# (`xattr -dr com.apple.quarantine .`) right where a user extracts, which
# is the fix for the per-dylib "allow this library" prompt storm on an
# unsigned/unnotarized download. Adding a FILE inside the tarball does not
# change the asset NAME, so the self-updater (which matches on name only)
# is unaffected; the SHA256SUMS digest is recomputed over final artifacts
# at release-assembly time, so it stays consistent.
cp packaging/macos/INSTALL-bundle.txt macos-stage/INSTALL.txt
tar -czf "${{ matrix.variant.bundle }}" -C "macos-stage" .
# The Homebrew edition: a SMALL tarball with just the bare (already rewritten
# + signed) binary and its install guide, NO bundled lib/. The binary is the
# same bytes as the bare self-update asset — it resolves its libs via the
# /opt/homebrew/lib rpath fallback bundle-macos.sh added, so a `brew install`
# user runs it with a fraction of the download. We build a CLEAN dir (copying
# only the single binary, never the whole stage) so the bundle's lib/ and the
# bundle INSTALL.txt can't leak in. FAIL-OPEN + the same staged-binary guard
# as the Pack step, and it reuses the built binary — never a fresh go build.
#
# Updater-safe by name: "asyncao-macos-homebrew-arm64.tar.gz" does not contain
# the darwin token "macos-arm64" (the "-homebrew-" breaks the run), so the
# self-updater never picks it (see update.SelfUpdateAssetMatch + the decoy in
# internal/update's TestSelfUpdatePicksSwappableDefault).
- name: Pack Homebrew edition tarball
continue-on-error: true
env:
ASSET: ${{ matrix.variant.asset }}
run: |
if [ ! -f "macos-stage/$ASSET" ]; then
echo "::warning::no staged binary (bundler failed earlier) — skipping the Homebrew tarball"
exit 1
fi
rm -rf macos-brew
mkdir -p macos-brew
cp "macos-stage/$ASSET" "macos-brew/$ASSET"
cp packaging/macos/INSTALL-homebrew.txt macos-brew/INSTALL.txt
tar -czf "${{ matrix.variant.homebrew }}" -C "macos-brew" .
- uses: actions/upload-artifact@v4
with:
# Three assets per variant: the bare self-update binary, the bundle
# tarball (binary + lib/ + INSTALL.txt), and the Homebrew edition tarball
# (binary + INSTALL.txt). download-artifact merge-multiple flattens them
# into dist/. A missing file (a failed continue-on-error step) is simply
# not uploaded (if-no-files-found default `warn`).
name: release-macos-${{ matrix.variant.id }}
path: |
${{ matrix.variant.asset }}
${{ matrix.variant.bundle }}
${{ matrix.variant.homebrew }}
build-flatpak:
name: Linux x86_64 Flatpak${{ matrix.variant.label }}
runs-on: ubuntu-latest
# AsyncAO is AGPLv3, so we ship a Flatpak too. This packaging was authored
# without a Linux box to test on, so the job is NON-BLOCKING (continue-on-error
# + the release job's success gates exclude it): a flatpak hiccup never holds up
# the release — iterate from these logs. Two variants like every other platform.
continue-on-error: true
strategy:
fail-fast: false
matrix:
variant:
- { id: default, label: "", tags: "", out: "AsyncAO-linux-x86_64.flatpak" }
- { id: nodiscord, label: " (Discord-free, no voice)", tags: "nodiscord novoice", out: "AsyncAO-linux-x86_64-nodiscord.flatpak" }
steps:
- uses: actions/checkout@v4
- name: Install flatpak + flatpak-builder + the runtime/SDK
run: |
sudo apt-get update
# elfutils provides eu-strip / eu-elfcompress, which flatpak-builder uses to
# strip module debuginfo — absent on the runner, the SDL2_ttf build fails at
# the strip step ("Failed to execute child process eu-strip").
sudo apt-get install -y --no-install-recommends flatpak flatpak-builder elfutils
flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install -y --user flathub \
org.freedesktop.Platform//24.08 \
org.freedesktop.Sdk//24.08 \
org.freedesktop.Sdk.Extension.golang//24.08
- name: Build the Flatpak bundle (version-stamped)
run: |
mkdir -p dist
# Substitute the template's build tags + version (see the manifest header),
# writing the generated manifest beside it so `path: ../..` still resolves.
gen=packaging/flatpak/_build.yaml
sed -e 's/@ASYNCAO_TAGS@/${{ matrix.variant.tags }}/g' \
-e 's/@ASYNCAO_VERSION@/${{ github.ref_name }}/g' \
packaging/flatpak/io.github.SyntaxNyah.AsyncAO.yaml > "$gen"
flatpak-builder --force-clean --user --repo=_repo _build "$gen"
flatpak build-bundle _repo "dist/${{ matrix.variant.out }}" io.github.SyntaxNyah.AsyncAO
- uses: actions/upload-artifact@v4
with:
name: release-flatpak-${{ matrix.variant.id }}
path: dist/${{ matrix.variant.out }}
release:
name: Publish GitHub Release
needs: [build-windows, build-linux, build-macos, build-flatpak]
runs-on: ubuntu-latest
# Only publish on a real tag push — a manual workflow_dispatch from a branch
# builds the binaries (handy for debugging the pipeline) but doesn't publish.
# build-flatpak is a dependency so its bundle is READY to attach, but it must
# never block the release: !cancelled() + the explicit core-platform success
# gates mean a flatpak failure is simply omitted, everything else still ships.
if: ${{ !cancelled() && startsWith(github.ref, 'refs/tags/') && needs.build-windows.result == 'success' && needs.build-linux.result == 'success' && needs.build-macos.result == 'success' }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # need the annotated tag's message for the release notes
- name: Collect all built assets
uses: actions/download-artifact@v4
with:
path: dist
pattern: release-*
merge-multiple: true
- name: Compute SHA256SUMS over the assembled (already-signed) assets
run: |
# Hash the FINAL artifacts here, at release-assembly time, so the
# published digest matches whatever shipped — including the Windows
# signature (signing happens inside build-windows on dist\asyncao.exe
# BEFORE it's named/zipped, so every downloaded artifact is already
# signed by now) and the macOS codesign. The in-app self-updater
# (internal/update.FetchSums) fetches this asset and verifies the
# download before the swap; releases cut before this step existed carry
# no SHA256SUMS.txt and the updater proceeds unverified (integrity-only).
#
# Globbed, never a hardcoded asset list, so a missing asset (e.g. the
# continue-on-error Flatpak) simply isn't hashed. Computed while the
# file does NOT yet live in dist/, so the manifest can't hash itself;
# then moved in so `gh release create dist/*` attaches it. Coreutils
# two-space "<hex> <name>" format — parseSums reads exactly this.
( cd dist && sha256sum * ) > SHA256SUMS.txt
mv SHA256SUMS.txt dist/
echo "----- SHA256SUMS.txt -----"; cat dist/SHA256SUMS.txt
- name: Build release notes from the CHANGELOG (fall back to the tag annotation)
run: |
# The Release notes are the version's CHANGELOG section — the SAME text the
# in-app "What's New" tab shows — so the two never drift and every feature for
# this version is listed. Falls back to the tag annotation, then a bare title.
awk -v ver="${{ github.ref_name }}" '
index($0, "## " ver) == 1 { grab=1; next }
grab && /^## / { exit }
grab { print }
' internal/ui/assets/CHANGELOG.md | sed '/./,$!d' > notes.md
if [ ! -s notes.md ]; then
git tag -l --format='%(contents)' "${{ github.ref_name }}" > notes.md
fi
if [ ! -s notes.md ]; then
echo "AsyncAO ${{ github.ref_name }}" > notes.md
fi
echo "----- release notes -----"; cat notes.md
echo "----- assets -----"; ls -la dist
- name: Create / update the GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
# Idempotent: drop any existing Release for this tag (the tag itself is kept) so
# a re-pushed tag re-publishes cleanly with the current notes + assets.
#
# Experimental channel automation: a HYPHENATED tag (v1.55.0-test.1 —
# cut from the MayAO-Test branch; tag pushes build whatever commit the
# tag points at, on any branch) publishes as a PRERELEASE. Stable
# clients never see it (/releases/latest excludes prereleases); the
# in-app experimental channel (Settings → Power user) follows it.
PRE=""
case "${{ github.ref_name }}" in
*-*) PRE="--prerelease" ;;
esac
gh release delete "${{ github.ref_name }}" --yes 2>/dev/null || true
gh release create "${{ github.ref_name }}" \
--title "AsyncAO ${{ github.ref_name }}" \
--notes-file notes.md \
--verify-tag \
$PRE \
dist/*