Skip to content

Merge pull request #3 from ghostapp-ai/dependabot/github_actions/acti… #11

Merge pull request #3 from ghostapp-ai/dependabot/github_actions/acti…

Merge pull request #3 from ghostapp-ai/dependabot/github_actions/acti… #11

Workflow file for this run

# Ghost CI/CD - Multiplatform Pipeline
#
# Jobs flow:
# checks --\
# audit ----+--> release --+--> build-desktop (4 matrix)
# test ----/ +--> build-android
# \--> build-ios (conditional)
name: Ghost CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
RUSTUP_MAX_RETRIES: 10
permissions:
contents: write
issues: write
pull-requests: write
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# QUALITY GATES — run on every push & PR
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
jobs:
# ─── Fast checks (no heavy Rust compilation, ~1-2min) ────────────
checks:
name: Checks
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/stub-pro
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: Rust format check
working-directory: src-tauri
run: cargo fmt --all -- --check
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install frontend deps
run: bun install --frozen-lockfile
- name: Frontend type-check & build
run: bun run build
# ─── Security audit (no compilation, ~30s) ───────────────────────
audit:
name: Audit
runs-on: ubuntu-22.04
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: taiki-e/install-action@cargo-audit
- name: Security audit
working-directory: src-tauri
run: cargo audit || true
# ─── Test + Clippy (heavy compilation, sccache + mold) ──────────
test:
name: Test & Clippy
runs-on: ubuntu-22.04
timeout-minutes: 20
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
RUSTFLAGS: "-C link-arg=-fuse-ld=mold"
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/stub-pro
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libwebkit2gtk-4.1-dev libappindicator3-dev \
librsvg2-dev patchelf libgtk-3-dev libsoup-3.0-dev \
libjavascriptcoregtk-4.1-dev mold
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Mozilla-Actions/sccache-action@v0.0.9
- uses: taiki-e/install-action@cargo-nextest
- name: Rust tests
working-directory: src-tauri
run: cargo nextest run
- name: Clippy (reuses sccache artifacts)
working-directory: src-tauri
run: cargo clippy --all-targets -- -D warnings
- name: sccache stats
run: sccache --show-stats
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# RELEASE — semantic-release on main push only
# Analyzes conventional commits → bumps version → CHANGELOG → tag
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
release:
name: Release
needs: [checks, audit, test]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
released: ${{ steps.semantic.outputs.new_release_published }}
version: ${{ steps.semantic.outputs.new_release_version }}
tag: ${{ steps.semantic.outputs.new_release_git_tag }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: true
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v6
id: semantic
with:
extra_plugins: |
@semantic-release/changelog
@semantic-release/git
@semantic-release/exec
conventional-changelog-conventionalcommits
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# DESKTOP BUILDS — Windows, macOS (ARM+Intel), Linux (all bundles)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
build-desktop:
name: Desktop (${{ matrix.label }})
needs: release
if: needs.release.outputs.released == 'true'
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- label: Windows x64
runner: windows-latest
target: x86_64-pc-windows-msvc
bundles: nsis
- label: macOS ARM64
runner: macos-latest
target: aarch64-apple-darwin
bundles: dmg
- label: macOS x64
runner: macos-latest
target: x86_64-apple-darwin
bundles: dmg
- label: Linux x64
runner: ubuntu-22.04
target: x86_64-unknown-linux-gnu
bundles: deb,appimage,rpm
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.release.outputs.tag }}
- uses: ./.github/actions/stub-pro
- name: Install Linux dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libwebkit2gtk-4.1-dev libappindicator3-dev \
librsvg2-dev patchelf libgtk-3-dev libsoup-3.0-dev \
libjavascriptcoregtk-4.1-dev mold
- name: Set mold linker for Linux
if: runner.os == 'Linux'
run: echo "RUSTFLAGS=-C link-arg=-fuse-ld=mold" >> "$GITHUB_ENV"
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Mozilla-Actions/sccache-action@v0.0.9
- name: Install frontend deps
run: bun install --frozen-lockfile
# ── Windows: force dynamic CRT (/MD) for all C++ compiled by
# llama-cpp-sys-2 (both CMake and cc::Build code paths).
# Without this, CMake Release defaults to /MT (static CRT →
# libcpmt.lib), which conflicts with Rust's /MD (dynamic CRT →
# msvcprt.lib) and causes LNK2005 "multiply defined symbols".
# CMAKE_MSVC_RUNTIME_LIBRARY is forwarded to CMake automatically
# by the llama-cpp-sys-2 build script (it forwards all CMAKE_* vars).
# CFLAGS/CXXFLAGS cover the cc::Build wrapper compilation step.
- name: Configure Windows CRT linkage
if: runner.os == 'Windows'
shell: bash
run: |
echo "CMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL" >> "$GITHUB_ENV"
echo "CFLAGS=/MD" >> "$GITHUB_ENV"
echo "CXXFLAGS=/MD" >> "$GITHUB_ENV"
# ── macOS signing: use real Apple certificate if configured,
# otherwise fall back to ad-hoc signing ("-").
# Ad-hoc is required on ARM (aarch64) or the app shows
# "App is damaged" — no Apple Developer account needed.
- name: Configure macOS signing
if: runner.os == 'macOS'
env:
APPLE_CERT_RAW: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERT_PASSWORD_RAW: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_ID_RAW: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID_RAW: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD_RAW: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID_RAW: ${{ secrets.APPLE_TEAM_ID }}
run: |
if [ -n "$APPLE_CERT_RAW" ]; then
echo "APPLE_CERTIFICATE=$APPLE_CERT_RAW" >> "$GITHUB_ENV"
echo "APPLE_CERTIFICATE_PASSWORD=$APPLE_CERT_PASSWORD_RAW" >> "$GITHUB_ENV"
echo "APPLE_SIGNING_IDENTITY=$APPLE_SIGNING_ID_RAW" >> "$GITHUB_ENV"
echo "APPLE_ID=$APPLE_ID_RAW" >> "$GITHUB_ENV"
echo "APPLE_PASSWORD=$APPLE_PASSWORD_RAW" >> "$GITHUB_ENV"
echo "APPLE_TEAM_ID=$APPLE_TEAM_ID_RAW" >> "$GITHUB_ENV"
echo "macOS signing: using Apple Developer certificate"
else
# Ad-hoc signing: codesign -s "-"
# tauri.conf.json already sets signingIdentity:"-",
# this env var takes precedence to be explicit.
echo "APPLE_SIGNING_IDENTITY=-" >> "$GITHUB_ENV"
echo "macOS signing: ad-hoc (no Apple Developer account configured)"
fi
- name: Build & upload artifacts
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tagName: ${{ needs.release.outputs.tag }}
releaseName: Ghost ${{ needs.release.outputs.tag }}
releaseDraft: false
prerelease: false
tauriScript: bun run tauri
args: --target ${{ matrix.target }} --bundles ${{ matrix.bundles }}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ANDROID BUILD — APK for aarch64 (covers 95%+ devices)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
build-android:
name: Android (aarch64)
needs: release
if: needs.release.outputs.released == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: write
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.release.outputs.tag }}
- uses: ./.github/actions/stub-pro
# ── Java 17 (required by Android Gradle) ──
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
# ── Android SDK + NDK ──
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Install Android SDK packages
run: |
yes | sdkmanager --licenses > /dev/null 2>&1 || true
sdkmanager --install \
"platforms;android-34" \
"ndk;27.0.12077973" \
"build-tools;34.0.0"
echo "NDK_HOME=$ANDROID_HOME/ndk/27.0.12077973" >> "$GITHUB_ENV"
# ── Rust with Android targets ──
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android
- uses: Mozilla-Actions/sccache-action@v0.0.9
# ── Gradle cache ──
- name: Cache Gradle
uses: actions/cache@v5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: gradle-${{ runner.os }}-
# ── Bun + frontend ──
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install frontend deps
run: bun install --frozen-lockfile
# ── Initialize & build ──
- name: Initialize Android project
run: bun run tauri android init
- name: Build Android APK
run: bun run tauri android build --target aarch64 --apk
# ── Detect keystore availability ──
- name: Detect Android keystore
env:
KEYSTORE_RAW: ${{ secrets.ANDROID_KEYSTORE }}
run: |
if [ -n "$KEYSTORE_RAW" ]; then
echo "HAS_KEYSTORE=true" >> "$GITHUB_ENV"
else
echo "HAS_KEYSTORE=false" >> "$GITHUB_ENV"
fi
# ── Sign APK (if keystore secrets configured) ──
- name: Sign APK
if: env.HAS_KEYSTORE == 'true'
env:
ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }}
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
echo "$ANDROID_KEYSTORE" | base64 --decode > /tmp/ghost-release.keystore
APK=$(find src-tauri/gen/android -name "*.apk" -type f | head -1)
echo "Found APK: $APK"
if [ -z "$APK" ]; then
echo "::error::No APK found!"
exit 1
fi
"$ANDROID_HOME/build-tools/34.0.0/apksigner" sign \
--ks /tmp/ghost-release.keystore \
--ks-pass "env:KEYSTORE_PASSWORD" \
--ks-key-alias "$KEY_ALIAS" \
--key-pass "env:KEY_PASSWORD" \
"$APK"
"$ANDROID_HOME/build-tools/34.0.0/apksigner" verify --verbose "$APK"
rm -f /tmp/ghost-release.keystore
# ── Upload to release ──
- name: Upload APK to release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ needs.release.outputs.version }}"
TAG="${{ needs.release.outputs.tag }}"
APK=$(find src-tauri/gen/android -name "*.apk" -type f | head -1)
if [ -z "$APK" ]; then
echo "::error::No APK found to upload!"
exit 1
fi
DEST="Ghost_${VERSION}_android-aarch64.apk"
cp "$APK" "$DEST"
gh release upload "$TAG" "$DEST" --clobber
- name: Upload workflow artifact
uses: actions/upload-artifact@v6
if: always()
with:
name: ghost-android-aarch64
path: Ghost_*.apk
retention-days: 7
if-no-files-found: ignore
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# iOS BUILD — always produces an unsigned .xcarchive (no Apple
# Developer account required). When Apple secrets ARE configured,
# additionally exports a signed .ipa for direct distribution.
#
# Background: iOS has NO equivalent of macOS ad-hoc signing.
# Every signed IPA needs a paid Apple Developer account ($99/yr).
# However, an unsigned .xcarchive can be sideloaded by users via
# AltStore / Sideloadly / their own free Apple ID in Xcode.
#
# Strategy:
# 1. tauri ios init → generate Xcode project
# 2. xcodebuild archive CODE_SIGNING_REQUIRED=NO → .xcarchive
# 3. Zip and upload .xcarchive (usable by all users)
# 4. IF Apple secrets configured → export signed .ipa + upload
#
# To enable signed IPA, configure these repository secrets:
# APPLE_CERTIFICATE, APPLE_CERTIFICATE_PASSWORD,
# APPLE_SIGNING_IDENTITY, APPLE_ID, APPLE_PASSWORD,
# APPLE_TEAM_ID, APPLE_PROVISIONING_PROFILE
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
build-ios:
name: iOS (aarch64)
needs: release
if: needs.release.outputs.released == 'true'
runs-on: macos-latest
timeout-minutes: 60
permissions:
contents: write
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.release.outputs.tag }}
- uses: ./.github/actions/stub-pro
# ── Rust with iOS target ──
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-ios
- uses: Mozilla-Actions/sccache-action@v0.0.9
# ── Bun + frontend ──
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install frontend deps
run: bun install --frozen-lockfile
# Detect whether Apple signing secrets are configured.
# secrets context is NOT allowed in step `if:` or `run:` expressions,
# so we expose it as an env var and check via shell test.
- name: Detect Apple certificate
env:
APPLE_CERT_RAW: ${{ secrets.APPLE_CERTIFICATE }}
run: |
if [ -n "$APPLE_CERT_RAW" ]; then
echo "HAS_APPLE_CERT=true" >> "$GITHUB_ENV"
else
echo "HAS_APPLE_CERT=false" >> "$GITHUB_ENV"
fi
# ── Generate Xcode project ──
- name: Initialize iOS project
run: bun run tauri ios init
# ── Build unsigned .xcarchive (no Apple account required) ──
# tauri ios build requires signing; we go directly to xcodebuild
# with CODE_SIGNING_REQUIRED=NO so CI passes without any cert.
# Reference: https://github.com/tauri-apps/tauri/issues/14940
- name: Build unsigned xcarchive
run: |
APP_NAME="ghost"
SCHEME="${APP_NAME}_iOS"
PROJECT="src-tauri/gen/apple/${APP_NAME}.xcodeproj"
ARCHIVE_PATH="src-tauri/gen/apple/build/App.xcarchive"
xcodebuild archive \
-project "$PROJECT" \
-scheme "$SCHEME" \
-archivePath "$ARCHIVE_PATH" \
-configuration Release \
-destination "generic/platform=iOS" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
| xcpretty || true
ls -la "src-tauri/gen/apple/build/" || true
# ── Upload unsigned .xcarchive (users can sideload via AltStore) ──
- name: Upload xcarchive to release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ needs.release.outputs.version }}"
TAG="${{ needs.release.outputs.tag }}"
ARCHIVE="src-tauri/gen/apple/build/App.xcarchive"
if [ ! -d "$ARCHIVE" ]; then
echo "::warning::xcarchive not found, skipping xcarchive upload"
exit 0
fi
DEST="Ghost_${VERSION}_ios-aarch64.xcarchive.zip"
ditto -c -k --sequesterRsrc --keepParent "$ARCHIVE" "$DEST"
gh release upload "$TAG" "$DEST" --clobber
echo "Uploaded unsigned xcarchive: $DEST"
echo "Users can sideload via AltStore/Sideloadly or sign with their own Apple Developer account."
# ── Install Apple certs (only if configured) ──
- name: Install Apple certificates
if: env.HAS_APPLE_CERT == 'true'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_PROVISIONING_PROFILE: ${{ secrets.APPLE_PROVISIONING_PROFILE }}
run: |
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -base64 32)"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
CERT_PATH="$RUNNER_TEMP/certificate.p12"
echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH"
security import "$CERT_PATH" \
-P "$APPLE_CERTIFICATE_PASSWORD" \
-A -t cert -f pkcs12 \
-k "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: \
-k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH"
if [ -n "$APPLE_PROVISIONING_PROFILE" ]; then
PP_PATH="$RUNNER_TEMP/profile.mobileprovision"
echo "$APPLE_PROVISIONING_PROFILE" | base64 --decode > "$PP_PATH"
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
PP_UUID=$(/usr/libexec/PlistBuddy -c "Print UUID" /dev/stdin \
<<< "$(security cms -D -i "$PP_PATH")" 2>/dev/null || true)
if [ -n "$PP_UUID" ]; then
cp "$PP_PATH" \
~/Library/MobileDevice/Provisioning\ Profiles/"$PP_UUID".mobileprovision
fi
fi
rm -f "$CERT_PATH"
# ── Export signed IPA from the .xcarchive (only if certs configured) ──
- name: Export signed IPA
if: env.HAS_APPLE_CERT == 'true'
env:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
ARCHIVE_PATH="src-tauri/gen/apple/build/App.xcarchive"
EXPORT_PATH="src-tauri/gen/apple/build/ipa-export"
# Write ExportOptions.plist for ad-hoc distribution
cat > /tmp/ExportOptions.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>method</key>
<string>ad-hoc</string>
<key>teamID</key>
<string>${APPLE_TEAM_ID}</string>
<key>signingStyle</key>
<string>automatic</string>
<key>stripSwiftSymbols</key>
<true/>
<key>compileBitcode</key>
<false/>
</dict>
</plist>
EOF
xcodebuild -exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportPath "$EXPORT_PATH" \
-exportOptionsPlist /tmp/ExportOptions.plist \
| xcpretty || true
# ── Upload signed IPA to release (only if signing succeeded) ──
- name: Upload IPA to release
if: env.HAS_APPLE_CERT == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ needs.release.outputs.version }}"
TAG="${{ needs.release.outputs.tag }}"
IPA=$(find src-tauri/gen/apple/build/ipa-export -name "*.ipa" -type f 2>/dev/null | head -1)
if [ -n "$IPA" ]; then
DEST="Ghost_${VERSION}_ios-aarch64.ipa"
cp "$IPA" "$DEST"
gh release upload "$TAG" "$DEST" --clobber
echo "Uploaded signed IPA: $DEST"
else
echo "::warning::IPA export failed or not found. xcarchive was already uploaded."
fi
# ── Upload xcarchive as workflow artifact too ──
- name: Upload workflow artifact
uses: actions/upload-artifact@v6
if: always()
with:
name: ghost-ios-aarch64-xcarchive
path: Ghost_*.xcarchive.zip
retention-days: 14
if-no-files-found: ignore
- name: Cleanup keychain
if: always()
run: security delete-keychain "$RUNNER_TEMP/app-signing.keychain-db" 2>/dev/null || true