Skip to content

feat(macos): native app menu, status item, window state, notifications #47

feat(macos): native app menu, status item, window state, notifications

feat(macos): native app menu, status item, window state, notifications #47

Workflow file for this run

name: Release
on:
workflow_dispatch: {}
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
permissions:
contents: write
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- leg: win
os: windows-latest
args: ''
- leg: linux-x64
os: ubuntu-22.04
args: ''
- leg: linux-arm64
os: ubuntu-22.04-arm
args: ''
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
cache: npm
- name: Setup Rust
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: "1.92.0"
targets: ${{ matrix.leg == 'mac' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Rust cache
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: src-tauri -> target
- name: Setup MSVC environment (Windows, for vendored OpenSSL build)
if: matrix.leg == 'win'
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1
- name: Install NASM (Windows, for vendored OpenSSL / SQLCipher)
if: matrix.leg == 'win'
shell: bash
run: |
choco install nasm -y
echo "C:\\Program Files\\NASM" >> "$GITHUB_PATH"
- name: Install Linux system dependencies
if: startsWith(matrix.leg, 'linux')
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libxdo-dev \
build-essential \
file \
perl \
xdg-utils
- name: Import Apple certificate
if: matrix.leg == 'mac' && env.APPLE_CERTIFICATE != ''
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
APPLE_SIGNING_IDENTITY_SECRET: ${{ secrets.APPLE_SIGNING_IDENTITY }}
run: |
echo "$APPLE_CERTIFICATE" | base64 --decode > cert.p12
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security import cert.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
rm cert.p12
if [ -n "$APPLE_SIGNING_IDENTITY_SECRET" ]; then
SIGNING_IDENTITY="$APPLE_SIGNING_IDENTITY_SECRET"
else
SIGNING_IDENTITY=$(security find-identity -v -p codesigning build.keychain | grep '"' | head -1 | sed 's/.*"\(.*\)"/\1/')
fi
echo "APPLE_SIGNING_IDENTITY=$SIGNING_IDENTITY" >> $GITHUB_ENV
- name: Write Apple API key
if: matrix.leg == 'mac' && env.APPLE_API_KEY_CONTENT != ''
env:
APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
run: |
mkdir -p ~/private_keys
echo "$APPLE_API_KEY_CONTENT" | base64 --decode > ~/private_keys/AuthKey_${APPLE_API_KEY}.p8
- name: Build @aster/ui
run: |
git clone --depth=1 https://github.com/Aster-Privacy/aster-ui.git ../aster-ui
cd ../aster-ui
npm ci --no-audit --no-fund
npm run build
- name: Fetch aster-crypto source (Aster-Mail)
run: git clone --depth=1 https://github.com/Aster-Privacy/Aster-Mail.git ../Aster-Mail
- name: Install frontend dependencies
run: npm ci --no-audit --no-fund
- name: Build and publish release
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY_PATH: ${{ secrets.APPLE_API_KEY != '' && format('~/private_keys/AuthKey_{0}.p8', secrets.APPLE_API_KEY) || '' }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tagName: v__VERSION__
releaseName: Aster Bridge __VERSION__
releaseDraft: false
prerelease: true
updaterJsonPreferNsis: true
args: ${{ matrix.args }}
tauriScript: npm run tauri
- name: Unbundle host graphics libraries from the AppImage
# linuxdeploy copies libwayland-client.so.0 and friends into the AppDir.
# They shadow the host copies, Mesa's libEGL_mesa.so.0 then fails to
# resolve wl_fixes_interface on any distribution newer than the runner,
# and WebKitGTK aborts with EGL_BAD_PARAMETER before the window paints.
if: startsWith(matrix.leg, 'linux')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
shell: bash
run: |
set -euo pipefail
sudo apt-get install -y squashfs-tools
V=$(node -p "require('./package.json').version")
TAG="v${V}"
shopt -s nullglob
for img in src-tauri/target/release/bundle/appimage/*.AppImage; do
bash scripts/fix_appimage_libs.sh "$img"
rm -f "$img.sig"
npm run tauri -- signer sign "$img"
gh release upload "$TAG" "$img" "$img.sig" --clobber
done
- name: Build and publish the Arch Linux package
# Arch has no .deb or .rpm, and the AppImage is not a package. Repack the
# release deb into a pacman package inside an Arch container so
# `pacman -U` installs Aster Bridge like any other Arch package.
if: matrix.leg == 'linux-x64'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
V=$(node -p "require('./package.json').version")
TAG="v${V}"
docker run --rm -e V="$V" -v "$PWD/packaging/arch:/pkg" archlinux:base-devel bash -c '
set -euo pipefail
pacman -Sy --noconfirm --needed pacman-contrib >/dev/null
useradd -m builder
cp /pkg/PKGBUILD /home/builder/PKGBUILD
chown -R builder:builder /home/builder
su builder -c "cd /home/builder && sed -i \"s/^pkgver=.*/pkgver=$V/\" PKGBUILD && updpkgsums && makepkg -f --noconfirm --nodeps"
cp /home/builder/*.pkg.tar.zst /pkg/
'
PKG="packaging/arch/aster-bridge-${V}-1-x86_64.pkg.tar.zst"
test -f "$PKG"
cp "$PKG" "packaging/arch/Aster-Bridge-x86_64.pkg.tar.zst"
gh release upload "$TAG" "$PKG" "packaging/arch/Aster-Bridge-x86_64.pkg.tar.zst" --clobber
- name: Rename assets to canonical names
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
V=$(node -p "require('./package.json').version")
TAG="v${V}"
TMP=$(mktemp -d)
rename_asset() {
local src="$1" dst="$2"
if gh release download "$TAG" --pattern "$src" --dir "$TMP" --clobber 2>/dev/null; then
mv "$TMP/$src" "$TMP/$dst"
gh release upload "$TAG" "$TMP/$dst" --clobber
fi
}
if [[ "${{ matrix.leg }}" == "linux-x64" ]]; then
rename_asset "Aster.Bridge_${V}_amd64.AppImage" "Aster-Bridge-x86_64.AppImage"
rename_asset "Aster.Bridge_${V}_amd64.AppImage.sig" "Aster-Bridge-x86_64.AppImage.sig"
rename_asset "Aster.Bridge_${V}_amd64.deb" "Aster-Bridge-amd64.deb"
rename_asset "Aster.Bridge_${V}_amd64.deb.sig" "Aster-Bridge-amd64.deb.sig"
rename_asset "Aster.Bridge-${V}-1.x86_64.rpm" "Aster-Bridge-x86_64.rpm"
rename_asset "Aster.Bridge-${V}-1.x86_64.rpm.sig" "Aster-Bridge-x86_64.rpm.sig"
elif [[ "${{ matrix.leg }}" == "linux-arm64" ]]; then
rename_asset "Aster.Bridge_${V}_aarch64.AppImage" "Aster-Bridge-aarch64.AppImage"
rename_asset "Aster.Bridge_${V}_aarch64.AppImage.sig" "Aster-Bridge-aarch64.AppImage.sig"
rename_asset "Aster.Bridge_${V}_arm64.deb" "Aster-Bridge-arm64.deb"
rename_asset "Aster.Bridge_${V}_arm64.deb.sig" "Aster-Bridge-arm64.deb.sig"
rename_asset "Aster.Bridge-${V}-1.aarch64.rpm" "Aster-Bridge-aarch64.rpm"
rename_asset "Aster.Bridge-${V}-1.aarch64.rpm.sig" "Aster-Bridge-aarch64.rpm.sig"
elif [[ "${{ matrix.leg }}" == "win" ]]; then
rename_asset "Aster.Bridge_${V}_x64-setup.exe" "Aster-Bridge-x64-setup.exe"
rename_asset "Aster.Bridge_${V}_x64-setup.exe.sig" "Aster-Bridge-x64-setup.exe.sig"
rename_asset "Aster.Bridge_${V}_x64_en-US.msi" "Aster-Bridge-x64.msi"
rename_asset "Aster.Bridge_${V}_x64_en-US.msi.sig" "Aster-Bridge-x64.msi.sig"
elif [[ "${{ matrix.leg }}" == "mac" ]]; then
rename_asset "Aster.Bridge_${V}_universal.dmg" "Aster-Bridge-universal.dmg"
rename_asset "Aster.Bridge_${V}_universal.dmg.sig" "Aster-Bridge-universal.dmg.sig"
rename_asset "Aster.Bridge_${V}_universal.app.tar.gz" "Aster-Bridge-universal.app.tar.gz"
rename_asset "Aster.Bridge_${V}_universal.app.tar.gz.sig" "Aster-Bridge-universal.app.tar.gz.sig"
fi
update_latest_json:
needs: build
# Runs even when a leg fails: the legs that DID succeed have already published
# their own partial latest.json, so skipping here would leave that partial file
# standing as the manifest. Running always means the guard below still fires.
if: always()
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Rebuild latest.json from every leg's signatures
# Each build leg's tauri-action uploads its own latest.json with --clobber,
# so the last leg to finish wins and the others' platforms vanish. v0.4.21
# shipped with only linux-aarch64 and Windows installs never saw the update.
# Rebuild the manifest from the signatures actually on the release instead
# of patching whichever partial file survived.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
V=$(node -p "require('./package.json').version")
TAG="v${V}"
TMP=$(mktemp -d)
BASE="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}"
if ! gh release view "$TAG" >/dev/null 2>&1; then
echo "::error::no release $TAG to build a manifest for; the build legs did not publish"
exit 1
fi
gh release download "$TAG" --pattern latest.json --dir "$TMP" --clobber
gh release download "$TAG" --pattern '*.sig' --dir "$TMP" --clobber || true
SIGDIR="$TMP" BASE="$BASE" V="$V" node -e '
const fs = require("fs");
const path = process.argv[1];
const dir = process.env.SIGDIR;
const base = process.env.BASE;
const v = process.env.V;
const prior = JSON.parse(fs.readFileSync(path, "utf8"));
const legs = {
"windows-x86_64": ["Aster-Bridge-x64-setup.exe", `Aster.Bridge_${v}_x64-setup.exe`],
"windows-x86_64-nsis": ["Aster-Bridge-x64-setup.exe", `Aster.Bridge_${v}_x64-setup.exe`],
"windows-x86_64-msi": [`Aster.Bridge_${v}_x64_en-US.msi`],
"linux-x86_64": ["Aster-Bridge-x86_64.AppImage", `Aster.Bridge_${v}_amd64.AppImage`],
"linux-x86_64-appimage": ["Aster-Bridge-x86_64.AppImage", `Aster.Bridge_${v}_amd64.AppImage`],
"linux-x86_64-deb": [`Aster.Bridge_${v}_amd64.deb`],
"linux-x86_64-rpm": [`Aster.Bridge-${v}-1.x86_64.rpm`],
"linux-aarch64": ["Aster-Bridge-aarch64.AppImage", `Aster.Bridge_${v}_aarch64.AppImage`],
"linux-aarch64-appimage": ["Aster-Bridge-aarch64.AppImage", `Aster.Bridge_${v}_aarch64.AppImage`],
"linux-aarch64-deb": [`Aster.Bridge_${v}_arm64.deb`],
"linux-aarch64-rpm": [`Aster.Bridge-${v}-1.aarch64.rpm`],
"darwin-aarch64": ["Aster-Bridge-universal.app.tar.gz", `Aster.Bridge_${v}_universal.app.tar.gz`],
"darwin-x86_64": ["Aster-Bridge-universal.app.tar.gz", `Aster.Bridge_${v}_universal.app.tar.gz`],
};
const manifest = {
version: prior.version || v,
notes: prior.notes || "",
pub_date: prior.pub_date || new Date().toISOString(),
platforms: {},
};
for (const [key, candidates] of Object.entries(legs)) {
const hit = candidates.find((f) => fs.existsSync(`${dir}/${f}.sig`));
if (!hit) {
console.log(`::warning::no signature for ${key} on this release; installs on that platform will not self-update`);
continue;
}
manifest.platforms[key] = {
signature: fs.readFileSync(`${dir}/${hit}.sig`, "utf8").trim(),
url: `${base}/${hit}`,
};
}
const platforms = Object.keys(manifest.platforms);
console.log("latest.json platforms:", platforms.join(", "));
const required = ["windows-x86_64", "linux-x86_64"];
const missing = required.filter((k) => !platforms.includes(k));
if (missing.length) {
console.log(`::error::latest.json is missing ${missing.join(", ")} - those installs would never self-update`);
process.exit(1);
}
if (!platforms.includes("linux-aarch64")) {
console.log("::warning::latest.json is missing linux-aarch64; ARM installs will not self-update");
}
if (!platforms.includes("darwin-aarch64")) {
console.log("::warning::no macOS build on this release; Mac installs stay on their current version");
}
fs.writeFileSync(path, JSON.stringify(manifest, null, 2));
' "$TMP/latest.json"
gh release upload "$TAG" "$TMP/latest.json" --clobber
ensure_download_assets:
needs: build
runs-on: ubuntu-22.04
steps:
- name: Ensure fixed-name bridge download assets exist
# release.yml builds Windows + Linux only; the verify_release_assets.yml guard
# cannot fire on a release created by GITHUB_TOKEN, so carry the macOS dmg over
# from a prior release here, in-run, to stop /api/bridge/v1/download/macos-dmg 404ing.
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
shell: bash
run: |
set -uo pipefail
V=$(node -p "require('./package.json').version" 2>/dev/null || echo "")
if [ -z "$V" ]; then V="${GITHUB_REF_NAME#v}"; fi
TAG="v${V}"
TMP="$(mktemp -d)"
REQUIRED=(
Aster-Bridge-x64-setup.exe
Aster-Bridge-x64.msi
Aster-Bridge-x86_64.AppImage
Aster-Bridge-amd64.deb
Aster-Bridge-x86_64.rpm
Aster-Bridge-universal.dmg
)
OPTIONAL=(
Aster-Bridge-aarch64.AppImage
Aster-Bridge-arm64.deb
Aster-Bridge-aarch64.rpm
Aster-Bridge-x86_64.pkg.tar.zst
)
declare -A SRC=(
[Aster-Bridge-x64-setup.exe]="Aster.Bridge_${V}_x64-setup.exe"
[Aster-Bridge-x64.msi]="Aster.Bridge_${V}_x64_en-US.msi"
[Aster-Bridge-x86_64.AppImage]="Aster.Bridge_${V}_amd64.AppImage"
[Aster-Bridge-amd64.deb]="Aster.Bridge_${V}_amd64.deb"
[Aster-Bridge-x86_64.rpm]="Aster.Bridge-${V}-1.x86_64.rpm"
[Aster-Bridge-aarch64.AppImage]="Aster.Bridge_${V}_aarch64.AppImage"
[Aster-Bridge-arm64.deb]="Aster.Bridge_${V}_arm64.deb"
[Aster-Bridge-aarch64.rpm]="Aster.Bridge-${V}-1.aarch64.rpm"
[Aster-Bridge-universal.dmg]="Aster.Bridge_${V}_universal.dmg"
[Aster-Bridge-x86_64.pkg.tar.zst]="aster-bridge-${V}-1-x86_64.pkg.tar.zst"
)
assets() { gh release view "$TAG" --repo "$REPO" --json assets -q '.assets[].name'; }
has() { assets | grep -qxF "$1"; }
for name in "${REQUIRED[@]}" "${OPTIONAL[@]}"; do
if has "$name"; then echo "ok: $name"; continue; fi
src="${SRC[$name]:-}"
if [ -n "$src" ] && has "$src"; then
echo "::notice::self-healing $name from $src on $TAG"
gh release download "$TAG" --repo "$REPO" --pattern "$src" --dir "$TMP" --clobber
mv "$TMP/$src" "$TMP/$name"
gh release upload "$TAG" "$TMP/$name" --repo "$REPO" --clobber
continue
fi
if [ "$name" = "Aster-Bridge-universal.dmg" ]; then
echo "::warning::no macOS build on $TAG; carrying over the dmg from the most recent release that has one"
for prev in $(gh release list --repo "$REPO" --limit 40 --json tagName,isDraft -q '.[] | select(.isDraft==false) | .tagName'); do
[ "$prev" = "$TAG" ] && continue
if gh release view "$prev" --repo "$REPO" --json assets -q '.assets[].name' | grep -qxF "Aster-Bridge-universal.dmg"; then
gh release download "$prev" --repo "$REPO" --pattern "Aster-Bridge-universal.dmg" --dir "$TMP" --clobber
gh release upload "$TAG" "$TMP/Aster-Bridge-universal.dmg" --repo "$REPO" --clobber
echo "::warning::carried over Aster-Bridge-universal.dmg from $prev - cut a fresh macOS build to replace it"
break
fi
done
continue
fi
done
ABSENT_OPTIONAL=()
for name in "${OPTIONAL[@]}"; do
has "$name" || ABSENT_OPTIONAL+=("$name")
done
if [ ${#ABSENT_OPTIONAL[@]} -ne 0 ]; then
echo "::warning::$TAG is missing optional assets: ${ABSENT_OPTIONAL[*]}. Those download links fall back to an older release."
fi
MISSING=()
for name in "${REQUIRED[@]}"; do
has "$name" || MISSING+=("$name")
done
if [ ${#MISSING[@]} -ne 0 ]; then
echo "::error::Release $TAG is missing bridge download assets: ${MISSING[*]}"
echo "The in-app Bridge downloads (/api/bridge/v1/download/<platform>) for these will 404. See the parent CLAUDE.md 'Website Download Assets'."
exit 1
fi
echo "All 6 bridge download assets present on $TAG."
gh release edit "$TAG" --repo "$REPO" --prerelease=false --latest
echo "Promoted $TAG to Latest."
attest_provenance:
needs: ensure_download_assets
if: always()
runs-on: ubuntu-22.04
continue-on-error: true
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Download the canonical installers
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
# Provenance ties each shipped installer to this commit, this workflow
# and this run. It is advisory: the job never blocks a release, so a
# transient signing failure cannot keep an installer off the site.
set -uo pipefail
V=$(node -p "require('./package.json').version" 2>/dev/null || echo "")
if [ -z "$V" ]; then V="${GITHUB_REF_NAME#v}"; fi
TAG="v${V}"
mkdir -p attest
for a in Aster-Bridge-x64-setup.exe Aster-Bridge-x64.msi Aster-Bridge-x86_64.AppImage Aster-Bridge-amd64.deb Aster-Bridge-x86_64.rpm Aster-Bridge-universal.dmg; do
gh release download "$TAG" --pattern "$a" --dir attest --clobber 2>/dev/null || \
echo "::warning::$a absent on $TAG, not attested"
done
ls -l attest || true
- name: Attest build provenance
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2
with:
subject-path: "attest/*"