Skip to content

chore(deps): pin brace-expansion to patched versions for audit gate (… #211

chore(deps): pin brace-expansion to patched versions for audit gate (…

chore(deps): pin brace-expansion to patched versions for audit gate (… #211

name: Nightly Release
# Fires on every push to main and on manual re-trigger.
# Produces a single rolling "nightly" prerelease DMG: one tag, one release,
# always pointing to the latest successful build on main.
on:
push:
branches:
- main
# workflow_dispatch lets you force a rebuild without a no-op commit,
# useful when a previous run failed due to a transient CI flake.
workflow_dispatch:
permissions:
contents: write
jobs:
guard:
name: Check if build needed
# Runs on a cheap ubuntu runner to avoid spinning up a macOS runner
# when the nightly tag already points to HEAD (duplicate push, bot commit, etc.).
runs-on: ubuntu-latest
outputs:
should_build: ${{ steps.check.outputs.should_build }}
# head_sha pins the exact commit this guard evaluated. The nightly job
# checks out this SHA explicitly so a concurrent push between jobs cannot
# cause the build and the tag to diverge.
head_sha: ${{ steps.check.outputs.head_sha }}
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
- name: Check nightly tag vs HEAD
id: check
run: |
NIGHTLY_SHA=$(git rev-parse nightly 2>/dev/null || echo "none")
HEAD_SHA=$(git rev-parse HEAD)
echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
# Manual triggers always build, regardless of tag state.
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "manual trigger, forcing build"
echo "should_build=true" >> "$GITHUB_OUTPUT"
elif [ "$NIGHTLY_SHA" = "$HEAD_SHA" ]; then
echo "nightly tag already at HEAD ($HEAD_SHA), skipping build"
echo "should_build=false" >> "$GITHUB_OUTPUT"
else
echo "should_build=true" >> "$GITHUB_OUTPUT"
fi
nightly:
name: Build and publish nightly DMG
needs: guard
if: needs.guard.outputs.should_build == 'true'
runs-on: macos-15
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
# Pin to the SHA guard evaluated, not the live HEAD. Without this, a
# second push landing between guard and this checkout would build a
# different commit than the one guard approved.
ref: ${{ needs.guard.outputs.head_sha }}
- 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
# The nightly toolchain with llvm-tools is required by cargo-llvm-cov
# to instrument binaries and enforce the 100% coverage gate.
- 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
# Give nightly a separate product identity so it coexists with stable:
# different productName/identifier → distinct /Applications path, TCC
# grant, Keychain service, and app-data dir. Updater stays off so the
# rolling "nightly" tag cannot try to self-update from latest.json.
- name: Patch tauri.conf.json for nightly identity
run: |
command -v jq >/dev/null 2>&1 || brew install jq
tmp="$(mktemp)"
jq '
.productName = "Thuki Nightly"
| .identifier = "com.quietnode.thuki.nightly"
| .plugins.updater.active = false
| .app.windows |= map(
if .title == "Thuki" then .title = "Thuki Nightly"
elif .title == "Thuki Settings" then .title = "Thuki Nightly Settings"
else .
end
)
' src-tauri/tauri.conf.json > "$tmp"
mv "$tmp" src-tauri/tauri.conf.json
jq '{productName, identifier, updater: .plugins.updater.active, titles: [.app.windows[].title]}' \
src-tauri/tauri.conf.json
- name: Build Tauri app
# VITE_GIT_COMMIT_SHA is set here, not on a separate frontend step, because
# tauri build runs beforeBuildCommand (bun run build:frontend) internally.
# A separate frontend build step would be overwritten by that second pass.
# AboutTab slices the SHA to 7 chars: v0.6.1+nightly.abc1234
env:
VITE_GIT_COMMIT_SHA: ${{ needs.guard.outputs.head_sha }}
run: bun run build:backend
# Ad-hoc signing with "-" satisfies Gatekeeper for drag-install without
# requiring an Apple Developer certificate in CI secrets. The app is not
# notarized, which is acceptable for a nightly/dev artifact.
# productName "Thuki Nightly" → bundle path has a space; quote carefully.
- name: Ad-hoc sign the app
run: |
APP_BUNDLE='src-tauri/target/release/bundle/macos/Thuki Nightly.app'
codesign --deep --force --sign - "$APP_BUNDLE"
codesign --verify --verbose "$APP_BUNDLE"
- name: Install create-dmg
run: brew install create-dmg
- name: Create DMG installer
run: |
# Stage only the .app. Asset filenames stay Thuki.dmg / Thuki.dmg.sig
# so install.sh can use the same names for stable and nightly.
mkdir -p /tmp/thuki-dmg-src
cp -R "src-tauri/target/release/bundle/macos/Thuki Nightly.app" /tmp/thuki-dmg-src/
mkdir -p src-tauri/target/release/bundle/dmg
create-dmg \
--volname "Thuki Nightly" \
--background "src-tauri/assets/dmg-background.png" \
--window-pos 200 120 \
--window-size 600 380 \
--icon-size 128 \
--icon "Thuki Nightly.app" 170 170 \
--hide-extension "Thuki Nightly.app" \
--app-drop-link 430 170 \
"src-tauri/target/release/bundle/dmg/Thuki.dmg" \
"/tmp/thuki-dmg-src"
rm -rf /tmp/thuki-dmg-src
# Same detached RSA scheme as release-please "Sign DMG for install script".
# Private key never leaves the secret; public key is pinned in install.sh.
- name: Sign DMG for install script
env:
INSTALLER_SIGNING_KEY: ${{ secrets.THUKI_INSTALLER_RSA_PRIVATE_KEY }}
run: |
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
# refs/tags/ is explicit to prevent ambiguity if a "nightly" branch
# is ever created alongside the tag.
- name: Force-push nightly tag
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -f nightly
git push origin refs/tags/nightly --force
# Floating release: delete the previous nightly release (if it exists)
# then recreate it. There is always exactly one nightly prerelease,
# always pointing to the latest build. Upload both DMG and signature.
- name: Publish nightly prerelease
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# "|| true" tolerates the first run when no nightly release exists yet.
gh release delete nightly --yes || true
SHORT_SHA="$(git rev-parse --short HEAD)"
NOTES="Automated build from main (${SHORT_SHA}).
Thuki Nightly is a separate app (Thuki Nightly.app, bundle id com.quietnode.thuki.nightly) that installs alongside stable Thuki. Not for production use.
Notes:
- Accessibility (and other TCC grants) may need to be re-granted for the nightly identity.
- Do not run stable and nightly at the same time: both claim double-tap Control and will fight over the hotkey.
- Auto-update is disabled for nightly; reinstall via the nightly channel to refresh."
gh release create nightly \
--title "Thuki Nightly" \
--notes "$NOTES" \
--prerelease \
src-tauri/target/release/bundle/dmg/Thuki.dmg \
src-tauri/target/release/bundle/dmg/Thuki.dmg.sig