ci(search): automate domain-credibility list refresh (#346) #287
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |