Something went wrong. Try again.
Hangar is your Bluesky home base—built natively for Linux. ✈️ hangar.blue
libadwaita gtk4 bluesky flatpak gnome atprotocol linux atproto appimage rust
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408# SPDX-License-Identifier: MPL-2.0
name: Release
on: push: branches: [main] workflow_dispatch: inputs: version: description: 'Release version (e.g., 0.2.0, 1.0.0). Leave empty for nightly.' required: false type: string
jobs: build-flatpak: name: Build Flatpak runs-on: ubuntu-latest container: image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-50 options: --privileged
steps: - name: Checkout uses: actions/checkout@v7
- name: Check the metainfo names this release if: inputs.version != '' run: | # Flatpak takes the ref's version from the newest <release> in the # metainfo, so a stale file mislabels the bundle everywhere it is # listed: flatpak info, GNOME Software, KDE Discover. NEWEST=$(grep -o '<release version="[^"]*"' data/io.github.sethcottle.Hangar.metainfo.xml \ | head -n1 | sed 's/.*version="//; s/"$//') if [ "$NEWEST" != "${{ inputs.version }}" ]; then echo "metainfo's newest release is ${NEWEST:-none}, building ${{ inputs.version }}" >&2 echo "add the release entry to data/io.github.sethcottle.Hangar.metainfo.xml" >&2 exit 1 fi
- name: Generate Cargo sources run: | pip3 install toml tomlkit aiohttp curl -sSL https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/cargo/flatpak-cargo-generator.py -o flatpak-cargo-generator.py python3 flatpak-cargo-generator.py Cargo.lock -o cargo-sources.json
- name: Apply nightly identity if: inputs.version == '' run: ./scripts/devel-identity.sh
- name: Stamp the build version run: | # Same HANGAR_VERSION as the AppImage, threaded through the # manifest's env block so the About dialog matches the release. if [ -n "${{ inputs.version }}" ]; then V="${{ inputs.version }}" else V="nightly-$(date +%Y%m%d)-${GITHUB_SHA::7}" fi sed -i "s/^ RUST_BACKTRACE: '1'$/ RUST_BACKTRACE: '1'\n HANGAR_VERSION: '${V}'/" io.github.sethcottle.Hangar.yml grep -q "HANGAR_VERSION" io.github.sethcottle.Hangar.yml || { echo "version stamp missed the manifest" >&2; exit 1; }
- name: Build Flatpak uses: flatpak/flatpak-github-actions/flatpak-builder@v6.7 with: manifest-path: io.github.sethcottle.Hangar.yml bundle: ${{ inputs.version != '' && 'hangar.flatpak' || 'hangar-nightly.flatpak' }} upload-artifact: false cache-key: flatpak-builder-${{ github.sha }}
- name: Upload Flatpak artifact uses: actions/upload-artifact@v7 with: name: hangar-flatpak path: hangar*.flatpak
build-appimage: name: Build AppImage runs-on: ubuntu-24.04
steps: - name: Checkout uses: actions/checkout@v7
- name: Install dependencies run: | sudo apt-get update sudo apt-get install -y \ libgtk-4-dev \ libadwaita-1-dev \ libsecret-1-dev \ librsvg2-dev \ libfuse2 \ libgstreamer1.0-dev \ libgstreamer-plugins-base1.0-dev \ binutils \ libglib2.0-bin \ libssl-dev
- name: Install Rust uses: dtolnay/rust-toolchain@stable
- name: Build release binary run: | # HANGAR_VERSION lands in the About dialog via build.rs. if [ -n "${{ inputs.version }}" ]; then HANGAR_VERSION="${{ inputs.version }}" cargo build --release else HANGAR_VERSION="nightly-$(date +%Y%m%d)-${GITHUB_SHA::7}" cargo build --release --features devel fi
- name: Check the glibc floor run: | # The AppImage bundles nothing, so the build host's glibc is the # floor it imposes on users. Rust std's process spawn pulls in # pidfd_spawnp (GLIBC_2.39), which no reading of the source shows, # so the binary is asked directly. Moving the runner to a newer # image must fail here rather than in someone's terminal. set -o pipefail FLOOR=2.39 command -v objdump >/dev/null || { echo "objdump missing; install binutils" >&2; exit 1; } MAX=$(objdump -T target/release/hangar \ | grep -o 'GLIBC_[0-9][0-9.]*' \ | sed 's/^GLIBC_//' \ | sort -V | tail -n1) # An empty MAX means the probe itself failed. Comparing it would # silently pass, which is the opposite of what this guard is for. [ -n "$MAX" ] || { echo "could not read any GLIBC requirement from the binary" >&2; exit 1; } echo "binary requires glibc $MAX, advertised floor $FLOOR" if [ "$(printf '%s\n%s\n' "$FLOOR" "$MAX" | sort -V | tail -n1)" != "$FLOOR" ]; then echo "the binary requires glibc $MAX but every advertised floor says $FLOOR" >&2 exit 1 fi
- name: Download appimagetool run: | curl -sSL https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -o appimagetool chmod +x appimagetool
- name: Determine app identity id: identity run: | if [ -n "${{ inputs.version }}" ]; then echo "app_id=io.github.sethcottle.Hangar" >> $GITHUB_OUTPUT else ./scripts/devel-identity.sh echo "app_id=io.github.sethcottle.Hangar.Devel" >> $GITHUB_OUTPUT fi
- name: Create AppDir structure run: | APP_ID="${{ steps.identity.outputs.app_id }}" mkdir -p AppDir/usr/bin mkdir -p AppDir/usr/share/applications mkdir -p AppDir/usr/share/icons/hicolor/scalable/apps mkdir -p AppDir/usr/share/metainfo mkdir -p AppDir/usr/share/glib-2.0/schemas
cp target/release/hangar AppDir/usr/bin/ cp "data/${APP_ID}.desktop" AppDir/usr/share/applications/ cp "data/${APP_ID}.metainfo.xml" AppDir/usr/share/metainfo/ cp "assets/icons/${APP_ID}.svg" AppDir/usr/share/icons/hicolor/scalable/apps/
# Window geometry lives in GSettings, and gio::Settings finds # nothing unless a compiled schema sits on XDG_DATA_DIRS, which # the AppRun points at this directory. The schema id does not # change between channels, so both copy the stable file. cp data/io.github.sethcottle.Hangar.gschema.xml AppDir/usr/share/glib-2.0/schemas/ glib-compile-schemas AppDir/usr/share/glib-2.0/schemas
# Raster sizes for desktops whose SVG renderer cannot draw the # scalable icon's filters (KDE turns them into black plates). for size in 16 32 48 64 128 256; do mkdir -p "AppDir/usr/share/icons/hicolor/${size}x${size}/apps" cp "assets/icons/png/${size}/${APP_ID}.png" "AppDir/usr/share/icons/hicolor/${size}x${size}/apps/" done
# Desktop file and icon symlinks at AppDir root (required by AppImage spec) ln -s "usr/share/applications/${APP_ID}.desktop" "AppDir/${APP_ID}.desktop" ln -s "usr/share/icons/hicolor/scalable/apps/${APP_ID}.svg" "AppDir/${APP_ID}.svg" # .DirIcon is used as the AppImage icon ln -s "usr/share/icons/hicolor/scalable/apps/${APP_ID}.svg" AppDir/.DirIcon
- name: Create AppRun script run: | printf '%s\n' '#!/bin/bash' > AppDir/AppRun printf '%s\n' '# AppRun for Hangar, a GTK4 + libadwaita app' >> AppDir/AppRun printf '%s\n' '#' >> AppDir/AppRun printf '%s\n' '# Hangar does NOT bundle GTK4/libadwaita libraries. It uses the' >> AppDir/AppRun printf '%s\n' '# host system libraries. Minimum: GTK 4.14, libadwaita 1.5 and' >> AppDir/AppRun printf '%s\n' '# glibc 2.39, i.e. GNOME 46+ (Fedora 40+, Ubuntu 24.04+, etc).' >> AppDir/AppRun printf '%s\n' '# GTK 4.14 is gtk_graphics_offload, libadwaita 1.5 is AdwDialog,' >> AppDir/AppRun printf '%s\n' '# and glibc 2.39 is the build host itself: all three are linked' >> AppDir/AppRun printf '%s\n' '# non-weak, so a host below any of them cannot start the binary.' >> AppDir/AppRun printf '%s\n' '#' >> AppDir/AppRun printf '%s\n' '# This is intentional: bundling GTK4/libadwaita from the build' >> AppDir/AppRun printf '%s\n' '# environment (Ubuntu 24.04) causes theme/rendering failures on' >> AppDir/AppRun printf '%s\n' '# newer distros due to library version mismatches.' >> AppDir/AppRun printf '%s\n' '#' >> AppDir/AppRun printf '%s\n' '# Video also needs the host GStreamer 1.20+. Unlike the media' >> AppDir/AppRun printf '%s\n' '# backend GTK used to dlopen, this one is linked in, so its' >> AppDir/AppRun printf '%s\n' '# absence stops the binary from starting at all rather than' >> AppDir/AppRun printf '%s\n' '# just disabling video. Hence the check below.' >> AppDir/AppRun printf '%s\n' '' >> AppDir/AppRun printf '%s\n' 'HERE="$(dirname "$(readlink -f "$0")")"' >> AppDir/AppRun printf '%s\n' '' >> AppDir/AppRun printf '%s\n' '# Make system data dirs available for icon and GSettings schema lookup,' >> AppDir/AppRun printf '%s\n' '# while also including our own data dir for the app icon and metainfo.' >> AppDir/AppRun printf '%s\n' 'export XDG_DATA_DIRS="${HERE}/usr/share:/usr/share:/usr/local/share:${XDG_DATA_DIRS:-/usr/share}"' >> AppDir/AppRun printf '%s\n' '' >> AppDir/AppRun printf '%s\n' '# Probe for it with ldconfig, which lives in /usr/sbin and is therefore NOT' >> AppDir/AppRun printf '%s\n' '# on the PATH a desktop launcher hands us, hence the absolute fallbacks.' >> AppDir/AppRun printf '%s\n' '# If no ldconfig can be found at all the probe proves nothing, and a probe' >> AppDir/AppRun printf '%s\n' '# that proves nothing must never be the reason the app refuses to start.' >> AppDir/AppRun printf '%s\n' 'LDCONFIG=""' >> AppDir/AppRun printf '%s\n' 'for candidate in ldconfig /sbin/ldconfig /usr/sbin/ldconfig; do' >> AppDir/AppRun printf '%s\n' ' if command -v "$candidate" >/dev/null 2>&1; then' >> AppDir/AppRun printf '%s\n' ' LDCONFIG="$candidate"' >> AppDir/AppRun printf '%s\n' ' break' >> AppDir/AppRun printf '%s\n' ' fi' >> AppDir/AppRun printf '%s\n' 'done' >> AppDir/AppRun printf '%s\n' '' >> AppDir/AppRun printf '%s\n' 'if [ -n "$LDCONFIG" ] && ! "$LDCONFIG" -p 2>/dev/null | grep -q "libgstgl-1\.0\.so\.0"; then' >> AppDir/AppRun printf '%s\n' ' echo "Hangar: the host GStreamer libraries are missing." >&2' >> AppDir/AppRun printf '%s\n' ' echo " Debian/Ubuntu: sudo apt install libgstreamer1.0-0 libgstreamer-plugins-base1.0-0 libgstreamer-gl1.0-0 gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav" >&2' >> AppDir/AppRun printf '%s\n' ' echo " Fedora: sudo dnf install gstreamer1 gstreamer1-plugins-base gstreamer1-plugins-good gstreamer1-plugins-bad-free gstreamer1-libav" >&2' >> AppDir/AppRun printf '%s\n' ' exit 1' >> AppDir/AppRun printf '%s\n' 'fi' >> AppDir/AppRun printf '%s\n' '' >> AppDir/AppRun printf '%s\n' '# A host below the floor cannot link. Say so before starting, using' >> AppDir/AppRun printf '%s\n' '# the same cheap ldconfig probe as above, then exec: a shell left in' >> AppDir/AppRun printf '%s\n' '# the middle would swallow SIGTERM and orphan the app at logout.' >> AppDir/AppRun printf '%s\n' 'if [ -n "$LDCONFIG" ]; then' >> AppDir/AppRun printf '%s\n' ' for lib in libadwaita-1.so.0 libgtk-4.so.1; do' >> AppDir/AppRun printf '%s\n' ' if ! "$LDCONFIG" -p 2>/dev/null | grep -q "$lib"; then' >> AppDir/AppRun printf '%s\n' ' echo "Hangar: $lib is missing." >&2' >> AppDir/AppRun printf '%s\n' ' echo " Hangar needs GTK 4.14, libadwaita 1.5 and glibc 2.39 or newer," >&2' >> AppDir/AppRun printf '%s\n' ' echo " which means GNOME 46 or later (Fedora 40+, Ubuntu 24.04+)." >&2' >> AppDir/AppRun printf '%s\n' ' exit 1' >> AppDir/AppRun printf '%s\n' ' fi' >> AppDir/AppRun printf '%s\n' ' done' >> AppDir/AppRun printf '%s\n' 'fi' >> AppDir/AppRun printf '%s\n' 'exec "${HERE}/usr/bin/hangar" "$@"' >> AppDir/AppRun chmod +x AppDir/AppRun
- name: Determine update channel id: update-channel run: | if [ -n "${{ inputs.version }}" ]; then echo "update_info=gh-releases-zsync|sethcottle|hangar|latest|Hangar*.AppImage.zsync" >> $GITHUB_OUTPUT echo "output_name=Hangar-x86_64.AppImage" >> $GITHUB_OUTPUT else echo "update_info=gh-releases-zsync|sethcottle|hangar|nightly|Hangar*.AppImage.zsync" >> $GITHUB_OUTPUT echo "output_name=Hangar-nightly-x86_64.AppImage" >> $GITHUB_OUTPUT fi
- name: Build AppImage run: | # Build the AppImage using appimagetool directly. # No linuxdeploy = no automatic library bundling. # Hangar relies on the host system's GTK4 + libadwaita. # # --appimage-extract-and-run: required in CI (no FUSE available) # -u: update information for delta updates via zsync # The output name is explicit so the nightly artifact is # distinct from the release one; the zsync file inherits it. ARCH=x86_64 ./appimagetool --appimage-extract-and-run \ -u "${{ steps.update-channel.outputs.update_info }}" \ AppDir "${{ steps.update-channel.outputs.output_name }}"
- name: Upload AppImage artifact uses: actions/upload-artifact@v7 with: name: hangar-appimage path: | Hangar*.AppImage Hangar*.AppImage.zsync
release: name: Create Release runs-on: ubuntu-latest needs: [build-flatpak, build-appimage] permissions: contents: write
steps: - name: Checkout uses: actions/checkout@v7
- name: Determine release type id: release-type run: | if [ -n "${{ inputs.version }}" ]; then echo "is_nightly=false" >> $GITHUB_OUTPUT echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT echo "tag_name=v${{ inputs.version }}" >> $GITHUB_OUTPUT else SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) DATE=$(date -u +%Y-%m-%d) echo "is_nightly=true" >> $GITHUB_OUTPUT echo "version=nightly" >> $GITHUB_OUTPUT echo "tag_name=nightly" >> $GITHUB_OUTPUT echo "date=$DATE" >> $GITHUB_OUTPUT echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT fi
- name: Download Flatpak artifact uses: actions/download-artifact@v8 with: name: hangar-flatpak
- name: Download AppImage artifact uses: actions/download-artifact@v8 with: name: hangar-appimage
- name: Delete existing nightly release if: steps.release-type.outputs.is_nightly == 'true' run: gh release delete nightly --yes || true env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Delete existing nightly tag if: steps.release-type.outputs.is_nightly == 'true' run: git push --delete origin nightly || true
- name: Create nightly release if: steps.release-type.outputs.is_nightly == 'true' uses: softprops/action-gh-release@v3 with: tag_name: nightly name: "Nightly ${{ steps.release-type.outputs.date }} (${{ steps.release-type.outputs.short_sha }})" body: | ## Nightly Build
**Date:** ${{ steps.release-type.outputs.date }} **Commit:** ${{ github.sha }}
This is an automated nightly build from the latest `main` branch. It may be unstable.
### Downloads
- **Flatpak**: `hangar-nightly.flatpak`. Install with `flatpak install hangar-nightly.flatpak` - **AppImage**: Make executable and run
### Authentication
Hangar uses OAuth for authentication. An app password fallback is also available. Create one at [bsky.app/settings/app-passwords](https://bsky.app/settings/app-passwords). files: | hangar*.flatpak Hangar*.AppImage Hangar*.AppImage.zsync draft: false prerelease: true
- name: Create versioned release if: steps.release-type.outputs.is_nightly == 'false' uses: softprops/action-gh-release@v3 with: tag_name: v${{ steps.release-type.outputs.version }} name: Hangar v${{ steps.release-type.outputs.version }} body: | ## Hangar v${{ steps.release-type.outputs.version }}
### Downloads
- **Flatpak**: `hangar.flatpak`. Install with `flatpak install hangar.flatpak` - **AppImage**: Make executable and run
### Authentication
Hangar uses OAuth for authentication. An app password fallback is also available. Create one at [bsky.app/settings/app-passwords](https://bsky.app/settings/app-passwords). files: | hangar*.flatpak Hangar*.AppImage Hangar*.AppImage.zsync draft: false prerelease: false
- name: Upload to Cloudflare R2 env: AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} run: | APPIMAGE=$(ls Hangar*.AppImage) ZSYNC=$(ls Hangar*.AppImage.zsync) # The local name is channel-dependent since the bundles went # channel-distinct; the AppImage globs already cover theirs. FLATPAK=$(ls hangar*.flatpak)
if [ "${{ steps.release-type.outputs.is_nightly }}" == "true" ]; then # Nightly builds use -nightly suffix aws s3 cp "$FLATPAK" s3://${{ secrets.R2_BUCKET_NAME }}/hangar/releases/hangar-nightly.flatpak \ --endpoint-url ${{ secrets.R2_ENDPOINT_URL }} aws s3 cp "$APPIMAGE" s3://${{ secrets.R2_BUCKET_NAME }}/hangar/releases/Hangar-nightly-x86_64.AppImage \ --endpoint-url ${{ secrets.R2_ENDPOINT_URL }} aws s3 cp "$ZSYNC" s3://${{ secrets.R2_BUCKET_NAME }}/hangar/releases/Hangar-nightly-x86_64.AppImage.zsync \ --endpoint-url ${{ secrets.R2_ENDPOINT_URL }} else # Versioned releases use standard names aws s3 cp "$FLATPAK" s3://${{ secrets.R2_BUCKET_NAME }}/hangar/releases/hangar.flatpak \ --endpoint-url ${{ secrets.R2_ENDPOINT_URL }} aws s3 cp "$APPIMAGE" s3://${{ secrets.R2_BUCKET_NAME }}/hangar/releases/Hangar-x86_64.AppImage \ --endpoint-url ${{ secrets.R2_ENDPOINT_URL }} aws s3 cp "$ZSYNC" s3://${{ secrets.R2_BUCKET_NAME }}/hangar/releases/Hangar-x86_64.AppImage.zsync \ --endpoint-url ${{ secrets.R2_ENDPOINT_URL }} fi