Skip to content

Release

Release #29

Workflow file for this run

name: Release
on:
workflow_dispatch:
inputs:
version:
description: "Release version"
required: true
type: string
permissions:
contents: write
# TODO: Uncomment when enabling SignPath code signing
# actions: read
# id-token: write
env:
CARGO_TERM_COLOR: always
MAKEFLAGS: -j4
jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set version
env:
VERSION: ${{ inputs.version }}
run: |
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
sed -i "s/^version = .*/version = \"$VERSION\"/" Cargo.toml
- name: Install system dependencies
# asciidoctor renders the man page from docs/cli/octa.1.adoc; bundled
# with the Linux tarball so `install.sh` can drop it into
# share/man/man1/. Same package on macOS via brew (asciidoctor) and
# on Arch via the standard repo, but Linux release is the only path
# that needs it in CI.
#
# The remaining packages are linuxdeploy / linuxdeploy-plugin-gtk
# runtime deps used in the "Build AppImage" step: patchelf rewrites
# rpaths in the bundled binary; gtk/gdk-pixbuf/rsvg *-bin packages
# supply the loader-cache tools the plugin invokes.
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libfontconfig1-dev libfreetype6-dev asciidoctor patchelf librsvg2-common libgdk-pixbuf2.0-bin libgtk-3-bin
- uses: dtolnay/rust-toolchain@stable
# No `cargo test` here. PR-time CI (ci.yml::test) already ran fmt + clippy
# + tests against this exact commit before merge; re-running on tag push
# would double the wall-time of every release without catching anything new.
- name: Build release binary
run: cargo build --release
- name: Render man page
run: asciidoctor -b manpage docs/cli/octa.1.adoc -o octa.1
- name: Package Linux release
run: |
DIR="octa-${VERSION}-linux-x86_64"
mkdir -p "$DIR/assets"
cp target/release/octa "$DIR/"
cp install.sh "$DIR/"
cp uninstall.sh "$DIR/"
cp octa.desktop "$DIR/"
cp assets/octa.svg "$DIR/assets/"
cp assets/octa.png "$DIR/assets/"
cp pkg/README-linux.md "$DIR/README.md"
cp LICENSE "$DIR/" 2>/dev/null || true
cp octa.1 "$DIR/"
tar czf "${DIR}.tar.gz" "$DIR/"
- name: Upload artifact
uses: actions/upload-artifact@v6
with:
name: linux-release
path: octa-${{ env.VERSION }}-linux-x86_64.tar.gz
- name: Download AppImage tooling
# linuxdeploy ships only as a rolling "continuous" release. Pinning by
# tag isn't an option; if a future linuxdeploy release breaks the build,
# swap to a specific dated tag from
# https://github.com/linuxdeploy/linuxdeploy/releases.
run: |
cd "$RUNNER_TEMP"
wget -q -O linuxdeploy https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
wget -q -O linuxdeploy-plugin-gtk https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh
chmod +x linuxdeploy linuxdeploy-plugin-gtk
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
- name: Build AppImage
env:
OUTPUT: Octa-${{ env.VERSION }}-x86_64.AppImage
# linuxdeploy-plugin-gtk can't auto-detect a GTK version when the
# ELF doesn't link libgtk directly (our binary only pulls libgtk
# transitively through eframe at runtime); set it explicitly to
# match the libgtk-3-dev we install above.
DEPLOY_GTK_VERSION: "3"
# APPIMAGE_EXTRACT_AND_RUN sidesteps FUSE — ubuntu-latest (24.04) no
# longer ships libfuse2 by default, and running AppImages without it
# otherwise fails with "dlopen(): error loading libfuse.so.2".
run: |
rm -rf AppDir
APPIMAGE_EXTRACT_AND_RUN=1 linuxdeploy \
--appdir AppDir \
--executable target/release/octa \
--desktop-file octa.desktop \
--icon-file assets/octa.png \
--plugin gtk \
--output appimage
# linuxdeploy derives the filename from the desktop Name field
# ("Octa-x86_64.AppImage"); rename to embed the version.
mv Octa-*.AppImage "$OUTPUT"
- name: Upload AppImage artifact
uses: actions/upload-artifact@v6
with:
name: linux-appimage
path: Octa-${{ env.VERSION }}-x86_64.AppImage
build-windows:
runs-on: windows-latest
# TODO: Uncomment when enabling SignPath code signing
# outputs:
# unsigned-artifact-id: ${{ steps.upload-unsigned.outputs.artifact-id }}
steps:
- uses: actions/checkout@v5
- name: Set version
env:
VERSION: ${{ inputs.version }}
shell: pwsh
run: |
echo "VERSION=$env:VERSION" >> $env:GITHUB_ENV
(Get-Content Cargo.toml) -replace '^version = .*', "version = `"$env:VERSION`"" | Set-Content Cargo.toml
- name: Generate ICO from PNG
run: magick assets/octa.png -define icon:auto-resize=256,128,64,48,32,16 assets/octa.ico
- uses: dtolnay/rust-toolchain@stable
# See linux comment: PR-time CI already covered tests on this commit.
- name: Build release binary
run: cargo build --release
- name: Package Windows release
shell: pwsh
run: |
$dir = "octa-$env:VERSION-windows-x86_64"
New-Item -ItemType Directory -Path "$dir\assets" -Force
Copy-Item "target\release\octa.exe" "$dir\"
Copy-Item "install.bat" "$dir\"
Copy-Item "assets\octa.svg" "$dir\assets\"
Copy-Item "assets\octa.png" "$dir\assets\"
if (Test-Path "assets\octa.ico") { Copy-Item "assets\octa.ico" "$dir\assets\" }
Copy-Item "pkg\README-windows.md" "$dir\README.md"
if (Test-Path "LICENSE") { Copy-Item "LICENSE" "$dir\" }
Compress-Archive -Path "$dir\*" -DestinationPath "octa-$env:VERSION-windows-x86_64.zip"
- name: Upload artifact
uses: actions/upload-artifact@v6
with:
name: windows-release
path: octa-${{ env.VERSION }}-windows-x86_64.zip
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v5
- name: Set version
env:
VERSION: ${{ inputs.version }}
run: |
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
sed -i '' "s/^version = .*/version = \"$VERSION\"/" Cargo.toml
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
# See linux comment: PR-time CI already covered tests on this commit.
- name: Build release binary
run: cargo build --release --target aarch64-apple-darwin
- name: Generate ICNS from PNG
run: |
ICONSET="assets/octa.iconset"
mkdir -p "$ICONSET"
for size in 16 32 64 128 256 512; do
sips -z $size $size assets/octa.png --out "$ICONSET/icon_${size}x${size}.png"
double=$((size * 2))
sips -z $double $double assets/octa.png --out "$ICONSET/icon_${size}x${size}@2x.png"
done
iconutil -c icns "$ICONSET" -o assets/octa.icns
- name: Build .app bundle
run: |
APP="Octa.app"
rm -rf "$APP"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
cp target/aarch64-apple-darwin/release/octa "$APP/Contents/MacOS/octa"
chmod +x "$APP/Contents/MacOS/octa"
cp assets/octa.icns "$APP/Contents/Resources/octa.icns"
cat > "$APP/Contents/Info.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key><string>en</string>
<key>CFBundleDisplayName</key><string>Octa</string>
<key>CFBundleExecutable</key><string>octa</string>
<key>CFBundleIconFile</key><string>octa</string>
<key>CFBundleIdentifier</key><string>dev.octa.Octa</string>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
<key>CFBundleName</key><string>Octa</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>${VERSION}</string>
<key>CFBundleVersion</key><string>${VERSION}</string>
<key>LSMinimumSystemVersion</key><string>11.0</string>
<key>NSHighResolutionCapable</key><true/>
<key>NSPrincipalClass</key><string>NSApplication</string>
</dict>
</plist>
EOF
- name: Ad-hoc codesign the bundle
run: |
# Sign the inner binary first, then seal the bundle. Ad-hoc (`-`) is
# not Developer ID — Gatekeeper still warns — but it converts the
# "Octa.app is damaged" dead-end into the standard "unidentified
# developer" dialog, where right-click → Open works.
codesign --force --sign - --timestamp=none \
--identifier dev.octa.Octa \
Octa.app/Contents/MacOS/octa
codesign --force --sign - --timestamp=none Octa.app
codesign --verify --verbose Octa.app
- name: Package macOS release
run: |
DIR="octa-${VERSION}-macos-aarch64"
mkdir -p "$DIR"
cp -R Octa.app "$DIR/"
cp pkg/README-macos.md "$DIR/README.md"
cp LICENSE "$DIR/" 2>/dev/null || true
tar czf "${DIR}.tar.gz" "$DIR/"
- name: Upload artifact
uses: actions/upload-artifact@v6
with:
name: macos-release
path: octa-${{ env.VERSION }}-macos-aarch64.tar.gz
publish:
needs: [build-linux, build-windows, build-macos]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/download-artifact@v7
with:
path: artifacts
- name: Create release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
run: |
NOTES_ARGS="--generate-notes"
if [ -s release_notes.md ]; then
NOTES_ARGS="--notes-file release_notes.md"
fi
gh release create "$VERSION" \
--repo "$GITHUB_REPOSITORY" \
--title "Octa $VERSION" \
$NOTES_ARGS \
artifacts/linux-release/*.tar.gz \
artifacts/linux-appimage/*.AppImage \
artifacts/windows-release/*.zip \
artifacts/macos-release/*.tar.gz
aur-publish:
needs: [publish]
runs-on: ubuntu-latest
environment:
name: aur
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Download tarballs and compute sha256
id: sha
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
run: |
# Source tarball (GitHub auto-generated archive)
curl -fL -o source.tar.gz \
"https://github.com/$GITHUB_REPOSITORY/archive/${VERSION}.tar.gz"
SOURCE_SHA256=$(sha256sum source.tar.gz | awk '{print $1}')
echo "source=$SOURCE_SHA256" >> "$GITHUB_OUTPUT"
echo "Source sha256: $SOURCE_SHA256"
# Binary tarball (release artifact)
gh release download "$VERSION" \
--repo "$GITHUB_REPOSITORY" \
--pattern "octa-${VERSION}-linux-x86_64.tar.gz" \
--output binary.tar.gz
BINARY_SHA256=$(sha256sum binary.tar.gz | awk '{print $1}')
echo "binary=$BINARY_SHA256" >> "$GITHUB_OUTPUT"
echo "Binary sha256: $BINARY_SHA256"
- name: Prepare PKGBUILDs
env:
VERSION: ${{ inputs.version }}
SOURCE_SHA256: ${{ steps.sha.outputs.source }}
BINARY_SHA256: ${{ steps.sha.outputs.binary }}
run: |
# octa (source)
sed -i "s/X\.X\.X/$VERSION/g" .github/aur/PKGBUILD .github/aur/.SRCINFO
sed -i "s/sha256sums=('SKIP')/sha256sums=('$SOURCE_SHA256')/" .github/aur/PKGBUILD
sed -i "s/sha256sums = SKIP/sha256sums = $SOURCE_SHA256/" .github/aur/.SRCINFO
# octa-bin (binary)
sed -i "s/X\.X\.X/$VERSION/g" .github/aur-bin/PKGBUILD .github/aur-bin/.SRCINFO
sed -i "s/sha256sums=('SKIP')/sha256sums=('$BINARY_SHA256')/" .github/aur-bin/PKGBUILD
sed -i "s/sha256sums = SKIP/sha256sums = $BINARY_SHA256/" .github/aur-bin/.SRCINFO
- name: Configure SSH for AUR
run: |
mkdir -p ~/.ssh
echo "${{ secrets.AUR_SSH_PRIVATE_KEY }}" > ~/.ssh/aur
chmod 600 ~/.ssh/aur
cat >> ~/.ssh/config <<'EOF'
Host aur.archlinux.org
IdentityFile ~/.ssh/aur
User aur
EOF
ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts
- name: Push octa to AUR
env:
VERSION: ${{ inputs.version }}
run: |
git -c init.defaultBranch=master clone \
ssh://aur@aur.archlinux.org/octa.git /tmp/aur-octa
cp .github/aur/PKGBUILD .github/aur/.SRCINFO /tmp/aur-octa/
cd /tmp/aur-octa
echo "--- PKGBUILD ---"
cat PKGBUILD
echo "--- .SRCINFO ---"
cat .SRCINFO
STATUS="$(git status --porcelain)"
echo "$STATUS" | grep -q 'PKGBUILD'
echo "$STATUS" | grep -q '.SRCINFO'
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add PKGBUILD .SRCINFO
git commit -m "release $VERSION"
git push
- name: Push octa-bin to AUR
env:
VERSION: ${{ inputs.version }}
run: |
git -c init.defaultBranch=master clone \
ssh://aur@aur.archlinux.org/octa-bin.git /tmp/aur-octa-bin
cp .github/aur-bin/PKGBUILD .github/aur-bin/.SRCINFO /tmp/aur-octa-bin/
cd /tmp/aur-octa-bin
echo "--- PKGBUILD ---"
cat PKGBUILD
echo "--- .SRCINFO ---"
cat .SRCINFO
STATUS="$(git status --porcelain)"
echo "$STATUS" | grep -q 'PKGBUILD'
echo "$STATUS" | grep -q '.SRCINFO'
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add PKGBUILD .SRCINFO
git commit -m "release $VERSION"
git push