Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions .github/actions/build-appimage/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
name: Build AppImage
description: |
Repackage a per-platform deadzone tarball into an AppImage.
Reads the tarball from the working directory, produces
`deadzone_<version>_linux_<arch>.AppImage` as an output.

`AppImage/appimagetool` release `1.9.1` is pinned here instead of
the `continuous` tag; bumping it is a conscious step (same philosophy
as TOKENIZERS_VERSION in release.yml). The older `AppImage/AppImageKit`
repo is archived and all its release-13 assets have been renamed
`obsolete-*` — don't use them.

inputs:
tarball:
description: Path (or glob) of the input tarball produced by the build job.
required: true
arch:
description: "`amd64` or `arm64` — matches the Linux build matrix."
required: true
icon:
description: Path to the 256x256 placeholder icon checked into the repo.
required: false
default: .github/assets/deadzone.png

outputs:
appimage:
description: Path to the produced AppImage file.
value: ${{ steps.build.outputs.appimage }}

runs:
using: composite
steps:
- name: Install appimagetool
shell: bash
env:
APPIMAGE_ARCH: ${{ inputs.arch }}
run: |
set -euo pipefail
# AppImage/appimagetool release 1.9.1 is the active successor
# to the archived AppImageKit repo (whose release-13 assets are
# now prefixed `obsolete-*`). Both x86_64 and aarch64 AppImages
# are published per release. Pinning (vs the `continuous` tag)
# means a bump is an explicit commit.
case "${APPIMAGE_ARCH}" in
amd64) tool_arch="x86_64" ;;
arm64) tool_arch="aarch64" ;;
*) echo "unsupported arch: ${APPIMAGE_ARCH}" >&2; exit 1 ;;
esac
url="https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-${tool_arch}.AppImage"
curl -fL -o /tmp/appimagetool "${url}"
chmod +x /tmp/appimagetool

- name: Install libfuse2
shell: bash
run: |
set -euo pipefail
# appimagetool is itself an AppImage and needs FUSE v2 to
# self-mount. Ubuntu 24.04 runners don't ship libfuse2 by
# default (FUSE v3 is the new default), so install it here.
# --appimage-extract-and-run would also work but costs an extra
# extract step per invocation.
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends libfuse2

- name: Assemble AppDir and build AppImage
id: build
shell: bash
env:
TARBALL_GLOB: ${{ inputs.tarball }}
APPIMAGE_ARCH: ${{ inputs.arch }}
ICON_PATH: ${{ inputs.icon }}
run: |
set -euo pipefail
# Resolve the tarball glob (download-artifact lands one file).
shopt -s nullglob
matches=( ${TARBALL_GLOB} )
if [[ ${#matches[@]} -ne 1 ]]; then
echo "::error::expected exactly 1 tarball matching '${TARBALL_GLOB}', got ${#matches[@]}"
printf ' %s\n' "${matches[@]}" >&2
exit 1
fi
tarball="${matches[0]}"

# Tarball naming: deadzone_<version>_linux_<arch>.tar.gz — the
# version segment is everything between `deadzone_` and
# `_linux_<arch>.tar.gz`, so peel both sides to recover it.
base="$(basename "${tarball}" .tar.gz)"
version="${base#deadzone_}"
version="${version%_linux_${APPIMAGE_ARCH}}"
if [[ -z "${version}" || "${version}" == "${base}" ]]; then
echo "::error::could not parse version from tarball name: ${base}" >&2
exit 1
fi

workdir="$(mktemp -d)"
tar -xzf "${tarball}" -C "${workdir}"
extracted="${workdir}/${base}"
if [[ ! -x "${extracted}/deadzone" ]]; then
echo "::error::extracted tarball missing executable deadzone binary" >&2
ls -la "${extracted}" >&2 || true
exit 1
fi

appdir="${workdir}/deadzone.AppDir"
mkdir -p "${appdir}/usr/bin"
cp "${extracted}/deadzone" "${appdir}/usr/bin/deadzone"
cp "${extracted}/LICENSE" "${extracted}/NOTICE" "${extracted}/README.md" "${appdir}/"

# AppRun wrapper. Explicitly steers DEADZONE_ORT_CACHE at the
# user's XDG cache dir — the AppImage payload is read-only at
# runtime (SquashFS mount), so letting the Go default land
# somewhere inside the mount would be a footgun. This matches
# internal/ort/ort.go's existing default, just lifted up so the
# contract is documented at the package boundary too.
cat > "${appdir}/AppRun" <<'APPRUN'
#!/bin/bash
set -e
HERE="$(dirname "$(readlink -f "$0")")"
export DEADZONE_ORT_CACHE="${DEADZONE_ORT_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/deadzone/ort}"
exec "${HERE}/usr/bin/deadzone" "$@"
APPRUN
chmod +x "${appdir}/AppRun"

# Minimal CLI .desktop file. Terminal=true tells launchers to
# spawn a terminal, but in practice deadzone is invoked from
# the shell or by an MCP client (stdio), so this is just spec
# compliance — not a desktop-integration feature.
cat > "${appdir}/deadzone.desktop" <<'DESKTOP'
[Desktop Entry]
Name=deadzone
Exec=deadzone
Icon=deadzone
Type=Application
Categories=Utility;Development;
Terminal=true
DESKTOP

# appimagetool looks for <AppName>.png (or .svg) at the AppDir
# root; a 256x256 placeholder is enough to satisfy validation.
cp "${GITHUB_WORKSPACE}/${ICON_PATH}" "${appdir}/deadzone.png"

output="deadzone_${version}_linux_${APPIMAGE_ARCH}.AppImage"
# ARCH env var is how appimagetool picks the embedded runtime;
# without it the tool guesses from the host, which is correct
# on the matrix but explicit is cheaper than debugging later.
case "${APPIMAGE_ARCH}" in
amd64) export ARCH="x86_64" ;;
arm64) export ARCH="aarch64" ;;
esac
# -n skips the GPG signing prompt (we don't sign yet; see #124
# out-of-scope). --no-appstream because this is a CLI with no
# AppStream metadata by design.
/tmp/appimagetool -n --no-appstream "${appdir}" "${output}"

ls -la "${output}"
echo "appimage=${output}" >> "$GITHUB_OUTPUT"
Binary file added .github/assets/deadzone.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
86 changes: 78 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,76 @@ jobs:
-artifacts ./empty-artifacts \
-db /tmp/deadzone-smoke.db

# Repackages each Linux tarball into an AppImage via the
# .github/actions/build-appimage composite. Runs after `smoke` so a
# broken tarball can't silently produce a broken AppImage (the tarball
# smoke is the cheaper canary). AppImage smoke (FUSE mount + ORT
# bootstrap) is inlined at the end of this job because the runner
# already has the AppImage on disk and libfuse2 installed via the
# composite — no sense adding a fourth job for two extra steps.
# Aggregation happens in `release` below, which globs `deadzone-*`
# artifacts; the `-appimage` suffix keeps AppImage and tarball
# artifacts from colliding on upload-artifact names.
appimage:
name: appimage (linux/${{ matrix.goarch }})
needs: smoke
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
goarch: amd64
- runner: ubuntu-24.04-arm
goarch: arm64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v6

- uses: actions/download-artifact@v4
with:
name: deadzone-linux-${{ matrix.goarch }}

- name: Build AppImage
id: build
uses: ./.github/actions/build-appimage
with:
tarball: deadzone_*_linux_${{ matrix.goarch }}.tar.gz
arch: ${{ matrix.goarch }}

# Mirrors the tarball smoke's contract (see the `smoke` job
# above): -version proves the AppImage launches and dylib linkage
# resolves inside the SquashFS mount, then `consolidate` with an
# empty artifacts dir exercises the ORT bootstrap (#73) from a
# read-only payload into $XDG_CACHE_HOME. If AppRun's
# DEADZONE_ORT_CACHE override regresses, this step is where it
# surfaces — the bootstrap would try to write inside the mount
# and fail.
- name: Smoke AppImage -version
shell: bash
run: |
set -euo pipefail
chmod +x "${{ steps.build.outputs.appimage }}"
"./${{ steps.build.outputs.appimage }}" -version

- name: Smoke AppImage consolidate (ORT bootstrap)
shell: bash
run: |
set -euo pipefail
mkdir -p empty-artifacts
"./${{ steps.build.outputs.appimage }}" consolidate \
-artifacts ./empty-artifacts \
-db /tmp/deadzone-appimage-smoke.db

- uses: actions/upload-artifact@v4
with:
name: deadzone-linux-${{ matrix.goarch }}-appimage
path: ${{ steps.build.outputs.appimage }}
if-no-files-found: error
retention-days: 7

release:
name: publish release
needs: [build, smoke]
needs: [build, smoke, appimage]
runs-on: ubuntu-24.04
# The job runs for both tag pushes and workflow_dispatch so the
# dry-run path can validate the tarball aggregation on a branch.
Expand Down Expand Up @@ -226,13 +293,15 @@ jobs:
run: |
set -euo pipefail
cd dist
# Single checksums.txt covers every per-platform tarball — users
# who want to verify hit one file, one command. The consolidated
# `deadzone.db` is uploaded separately by `deadzone dbrelease`
# from the operator's laptop after this job finishes (see #101
# §H); its sha256 lives in a sibling `deadzone.db.sha256` asset
# written by that subcommand.
sha256sum deadzone_*.tar.gz > "deadzone_${VERSION}_checksums.txt"
# Single checksums.txt covers every per-platform tarball and
# AppImage — users who want to verify hit one file, one
# command. The consolidated `deadzone.db` is uploaded
# separately by `deadzone dbrelease` from the operator's
# laptop after this job finishes (see #101 §H); its sha256
# lives in a sibling `deadzone.db.sha256` asset written by
# that subcommand.
sha256sum deadzone_*.tar.gz deadzone_*.AppImage \
> "deadzone_${VERSION}_checksums.txt"
cat "deadzone_${VERSION}_checksums.txt"

- name: Create GitHub release
Expand All @@ -249,4 +318,5 @@ jobs:
--title "$VERSION" \
--generate-notes \
dist/deadzone_*.tar.gz \
dist/deadzone_*.AppImage \
"dist/deadzone_${VERSION}_checksums.txt"
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Documentation is fetched by the `deadzone scrape` subcommand, embedded into vect

Pre-built binaries for **macOS Apple Silicon**, **Linux amd64**, and **Linux arm64** are published on the [Releases page](https://github.com/laradji/deadzone/releases). Windows is blocked upstream (no `libtokenizers.a`). If you want to build from source instead — most useful if you're contributing or running on an unsupported platform — skip to [Build from source](#build-from-source).

macOS Apple Silicon users can also install via [Homebrew](#homebrew-macos-apple-silicon) below — it's a one-liner and skips the quarantine workaround.
macOS Apple Silicon users can also install via [Homebrew](#homebrew-macos-apple-silicon) below — it's a one-liner and skips the quarantine workaround. Linux users have a one-liner too: the [AppImage](#appimage-linux) bundles the binary and its assets into a single self-mounting file.

### Homebrew (macOS Apple Silicon)

Expand All @@ -68,6 +68,23 @@ Apple Silicon only — Intel Macs aren't built by the release pipeline. If you n

Homebrew installs into a non-quarantined location, so the [quarantine workaround](#macos-clear-the-quarantine-attribute) below doesn't apply.

### AppImage (Linux)

```bash
VERSION=v0.1.0
ARCH=amd64 # or arm64

curl -L -O "https://github.com/laradji/deadzone/releases/download/${VERSION}/deadzone_${VERSION}_linux_${ARCH}.AppImage"
chmod +x "deadzone_${VERSION}_linux_${ARCH}.AppImage"
"./deadzone_${VERSION}_linux_${ARCH}.AppImage" -version
```

The AppImage self-mounts its payload via FUSE v2. Most desktop distros have `libfuse2` preinstalled; minimal server or container images often don't. If you hit `dlopen(): error loading libfuse.so.2`, either install the FUSE v2 package (`apt-get install libfuse2` on Debian/Ubuntu, `dnf install fuse-libs` on Fedora) or pass `--appimage-extract-and-run`, which bypasses FUSE entirely by extracting the payload to a temp dir per invocation:

```bash
"./deadzone_${VERSION}_linux_${ARCH}.AppImage" --appimage-extract-and-run -version
```

### Quick install

Pick the archive for your platform and extract it into the directory you want to run deadzone from:
Expand Down