Skip to content

ci(search): automate domain-credibility list refresh (#346) #287

ci(search): automate domain-credibility list refresh (#346)

ci(search): automate domain-credibility list refresh (#346) #287

name: Release Please
on:
push:
branches:
- main
# Manually (re)build and upload assets for an existing release tag, e.g. when
# a prior run failed after release-please had already created the release. The
# release-please job is a no-op on dispatch (the release already exists), and
# the build job is gated to run on dispatch via github.event_name below.
workflow_dispatch:
inputs:
tag:
description: 'Existing release tag to build and upload assets for (e.g. v0.15.0)'
required: true
type: string
permissions:
contents: write
pull-requests: write
jobs:
release-please:
name: Open or update release PR
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
pr: ${{ steps.release.outputs.pr }}
steps:
- uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
id: release
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
sync-cargo-lock:
name: Sync Cargo.lock into release PR
needs: release-please
# Runs when release-please opened/updated a PR but has not yet created a release
if: ${{ !needs.release-please.outputs.release_created && needs.release-please.outputs.pr }}
runs-on: ubuntu-latest
steps:
- name: Checkout release PR branch
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ fromJSON(needs.release-please.outputs.pr).headBranchName }}
- name: Install stable Rust toolchain
run: rustup toolchain install stable --no-self-update
- name: Update Cargo.lock for thuki package
working-directory: src-tauri
run: cargo update --package thuki
- name: Commit Cargo.lock if it changed
run: |
if ! git diff --quiet src-tauri/Cargo.lock; then
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add src-tauri/Cargo.lock
git commit -m "chore: sync Cargo.lock to version bump"
git push
else
echo "Cargo.lock already in sync, nothing to commit"
fi
build-and-release:
name: Build and publish macOS app
needs: release-please
if: ${{ needs.release-please.outputs.release_created || github.event_name == 'workflow_dispatch' }}
runs-on: macos-15
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# On manual dispatch, build the exact released source for the requested
# tag. On push, this is empty and checkout uses the triggering commit.
ref: ${{ inputs.tag }}
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.11
- name: Install stable Rust toolchain
run: rustup toolchain install stable --no-self-update
- name: Install nightly Rust toolchain
run: rustup toolchain install nightly-2026-03-30 --component llvm-tools --no-self-update
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2
with:
tool: cargo-llvm-cov
- name: Install frontend dependencies
run: bun install --frozen-lockfile
# The sidecar must be present before any cargo invocation. lint:backend
# (cargo clippy) and test:all:coverage (cargo llvm-cov) both run build.rs,
# which fails if binaries/llama-server-<triple> is missing. Restore the
# cache and build on miss before the first lint step, not after.
- name: Cache llama.cpp sidecar
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: src-tauri/binaries
key: llama-cpp-${{ runner.os }}-${{ hashFiles('scripts/ensure-llama-server.ts') }}
- name: Build llama-server sidecar
run: bun run engine:ensure
- name: Lint and format check
run: bun run lint && bun run format:check
- name: Typecheck
run: bun run typecheck
- name: Run all tests with coverage enforcement
run: bun run test:all:coverage
- name: Build frontend
run: bun run build:frontend
- name: Build Tauri app
run: bun run build:backend
- name: Ad-hoc sign the app
run: |
codesign --deep --force --sign - src-tauri/target/release/bundle/macos/Thuki.app
codesign --verify --verbose src-tauri/target/release/bundle/macos/Thuki.app
- name: Pack and sign updater payload
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
TAG: ${{ needs.release-please.outputs.tag_name || inputs.tag }}
working-directory: src-tauri/target/release/bundle/macos
run: |
VERSION="${TAG#v}"
PAYLOAD="Thuki_${VERSION}_aarch64.app.tar.gz"
tar czf "$PAYLOAD" Thuki.app
# Sign the payload with the project ed25519 key. The CLI reads the
# key + password from TAURI_SIGNING_PRIVATE_KEY[_PASSWORD] env vars.
bunx --bun @tauri-apps/cli signer sign "$PAYLOAD"
- name: Generate updater manifest
env:
TAG: ${{ needs.release-please.outputs.tag_name || inputs.tag }}
working-directory: src-tauri/target/release/bundle/macos
run: |
VERSION="${TAG#v}"
PAYLOAD="Thuki_${VERSION}_aarch64.app.tar.gz"
SIG="$(cat "${PAYLOAD}.sig")"
PUB_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
DOWNLOAD_URL="https://github.com/quiet-node/thuki/releases/download/${TAG}/${PAYLOAD}"
# Embed the FULL released changelog (every version section, not just
# this tag's) so the in-app "What's New" window can show every release
# a user skipped over. The window parses these headers and slices to
# the user's installed version client-side. We take everything from
# the first `## [x.y.z]` header to EOF, which drops the document title,
# the preamble, and the "## Unreleased" block (all of which sit above
# the first version header). An empty result means CHANGELOG.md has no
# released sections yet: fail loudly rather than ship blank notes.
awk '/^## \[[0-9]+\.[0-9]+\.[0-9]+\]/ {found=1} found {print}' \
"$GITHUB_WORKSPACE/CHANGELOG.md" > release-notes.md
if [ ! -s release-notes.md ]; then
echo "::error::CHANGELOG.md has no released version sections; refusing to publish a manifest with no notes." >&2
exit 1
fi
# jq --rawfile keeps newlines, quotes, backticks and $ in the
# changelog JSON-safe (the old heredoc interpolation could not).
jq -n \
--arg version "$VERSION" \
--rawfile notes release-notes.md \
--arg pub_date "$PUB_DATE" \
--arg sig "$SIG" \
--arg url "$DOWNLOAD_URL" \
'{version: $version, notes: $notes, pub_date: $pub_date, platforms: {"darwin-aarch64": {signature: $sig, url: $url}}}' \
> latest.json
rm -f release-notes.md
- name: Install create-dmg
run: brew install create-dmg
- name: Create DMG installer
run: |
# Stage only the .app — exclude any leftover build artifacts
# (e.g. Thuki.app.tar.gz) from the bundle directory.
mkdir -p /tmp/thuki-dmg-src
cp -r src-tauri/target/release/bundle/macos/Thuki.app /tmp/thuki-dmg-src/
mkdir -p src-tauri/target/release/bundle/dmg
create-dmg \
--volname "Thuki" \
--background "src-tauri/assets/dmg-background.png" \
--window-pos 200 120 \
--window-size 600 380 \
--icon-size 128 \
--icon "Thuki.app" 170 170 \
--hide-extension "Thuki.app" \
--app-drop-link 430 170 \
"src-tauri/target/release/bundle/dmg/Thuki.dmg" \
"/tmp/thuki-dmg-src"
rm -rf /tmp/thuki-dmg-src
- name: Sign DMG for install script
env:
INSTALLER_SIGNING_KEY: ${{ secrets.THUKI_INSTALLER_RSA_PRIVATE_KEY }}
run: |
# Detached RSA (PKCS#1 v1.5, SHA-256) signature over the DMG. The
# curl | sh installer verifies it with stock macOS `openssl` against a
# public key pinned in the script. The private key exists only in this
# CI secret, never on the release, so a DMG swapped on a compromised
# release fails verification: the attacker cannot forge the signature.
KEYFILE="$(mktemp)"
trap 'rm -f "$KEYFILE"' EXIT
printf '%s\n' "$INSTALLER_SIGNING_KEY" > "$KEYFILE"
openssl dgst -sha256 -sign "$KEYFILE" \
-out src-tauri/target/release/bundle/dmg/Thuki.dmg.sig \
src-tauri/target/release/bundle/dmg/Thuki.dmg
- name: Upload release assets
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.release-please.outputs.tag_name || inputs.tag }}
working-directory: src-tauri/target/release/bundle
run: |
VERSION="${TAG#v}"
PAYLOAD="Thuki_${VERSION}_aarch64.app.tar.gz"
gh release upload "$TAG" \
"dmg/Thuki.dmg" \
"dmg/Thuki.dmg.sig" \
"macos/${PAYLOAD}" \
"macos/${PAYLOAD}.sig" \
"macos/latest.json"
# Publish (un-draft) the release ONLY after every asset is uploaded.
# release-please creates the release as a draft (draft: true in
# release-please-config.json), so it stays hidden from
# releases/latest while this macOS job builds and uploads. Flipping
# the draft here is the atomic go-live: until this runs, the install
# script and the in-app updater keep resolving the previous good
# release instead of 404ing on a release whose DMG does not exist yet.
# No `if: always()` on purpose: a failed build leaves an unpublished
# draft and releases/latest stays on the last good version.
- name: Publish release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.release-please.outputs.tag_name || inputs.tag }}
run: gh release edit "$TAG" --draft=false