From 1ce19f42f60d046cdacb989568ebef855583e933 Mon Sep 17 00:00:00 2001 From: sumerc Date: Thu, 21 May 2026 00:39:44 +0300 Subject: [PATCH 1/5] initiahl sh-releaser code(tested locally) + rmove dead flags --- .github/workflows/release.yml | 31 +------------------------------ .goreleaser.yml | 13 ------------- CHANGELOG.md | 15 +++++++++++++++ CLAUDE.md | 23 +++++++++++++---------- README.md | 33 +++++++++++---------------------- main.go | 7 ------- test/integration_test.go | 10 +++++----- transcriber/fake.go | 24 ++++++++++++++++++------ 8 files changed, 63 insertions(+), 93 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 575aed6..791c951 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,34 +67,5 @@ jobs: chmod +x "$universal" packaging/mkdmg.sh "$universal" "$VERSION" "Zee-${VERSION}.dmg" shasum -a 256 "Zee-${VERSION}.dmg" >> dist/checksums.txt - gh release upload "$VERSION" "Zee-${VERSION}.dmg" --clobber + gh release upload "$VERSION" "Zee-${VERSION}.dmg" dist/checksums.txt --clobber - - name: Update Homebrew cask - env: - VERSION: ${{ github.ref_name }} - GITHUB_TOKEN: ${{ secrets.ZEE_RELEASE_TOKEN }} - run: | - CLEAN_VERSION="${VERSION#v}" - SHA256=$(shasum -a 256 "Zee-${VERSION}.dmg" | awk '{print $1}') - cat > zee.rb <.dmg` -2. Open the DMG and drag `Zee.app` to `/Applications` -3. On first launch, grant **Microphone** and **Accessibility** permissions to the terminal or Zee.app via System Settings > Privacy & Security -4. Set `GROQ_API_KEY` in your shell profile or launch via: `GROQ_API_KEY=xxx open /Applications/Zee.app` +End users install via: + +```bash +curl -fsSL https://raw.githubusercontent.com/sumerc/zee/main/install.sh | bash +``` + +`install.sh` downloads the latest DMG from GitHub Releases, verifies its SHA256 against `checksums.txt`, copies `Zee.app` to `/Applications`, and runs `xattr -cr` to clear quarantine. Permissions (Microphone, Accessibility) are still granted lazily by macOS TCC on first use — installers cannot pre-grant them. + +Local dev DMG: `make app` produces `Zee-.dmg`; drag to `/Applications`. ## Testing @@ -32,7 +37,6 @@ make benchmark WAV=file.wav RUNS=5 ## Flags -- `-stream` - enable streaming transcription (Deepgram only) - `-debug` - enable diagnostic logging (default: false) - `-debug-transcribe` - enable transcription text logging (requires `-debug`) - `-format ` - audio format (default: mp3@16) @@ -106,12 +110,11 @@ git tag v0.3.0 && git push origin v0.3.0 # triggers CI release CI (`.github/workflows/release.yml`) does: 1. Verify tag is on `main` 2. GoReleaser builds arm64 + amd64 binaries, universal binary, tar.gz archives, checksums, GitHub release -3. GoReleaser pushes Homebrew formula to `sumerc/homebrew-tap` -4. Post-step creates DMG from universal binary and uploads to release +3. Post-step creates DMG from universal binary, appends its SHA256 to `checksums.txt`, uploads to release -Requires `ZEE_RELEASE_TOKEN` repo secret (fine-grained PAT with Contents read/write on `zee` + `homebrew-tap`). +Requires `ZEE_RELEASE_TOKEN` repo secret (fine-grained PAT with Contents read/write on `zee`). -Users install via: `brew install sumerc/tap/zee` +Users install via the one-liner in README (`install.sh` fetches the DMG and verifies the checksum). ## Packaging diff --git a/README.md b/README.md index 3ead5c8..ec97158 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ - **System tray app** — lives in the menu bar. Switch microphones, transcription providers, and languages from the tray menu. Dynamic icons show recording and warning states. - **Two recording modes** — push-to-talk (hold hotkey) or tap-to-toggle (tap to start/stop). -- **Real-time streaming** — with `-stream`, words appear as you speak and auto-paste into the focused window incrementally. Powered by Deepgram's WebSocket API. +- **Real-time streaming** — when a streaming-capable model is selected (e.g. Deepgram Nova-3), words appear as you speak and auto-paste into the focused window incrementally. - **Fast batch mode** — HTTP keep-alive, TLS connection reuse, pre-warmed connections, streaming encoder runs during recording (not after). Typical key-release to clipboard: under 500ms. - **Auto-paste** — transcribed text goes straight to clipboard and pastes into the active window. In streaming mode, each new phrase pastes as it arrives. - **Silence detection** — VAD-based voice activity detection warns when no speech is heard. In streaming mode, auto-closes recording after 30 seconds of silence. @@ -31,35 +31,26 @@ ## Install -### Homebrew (recommended) +### One-liner (recommended) ```bash -brew install --cask sumerc/tap/zee +curl -fsSL https://raw.githubusercontent.com/sumerc/zee/main/install.sh | bash ``` -Installs `Zee.app` to `/Applications`. Launch from Spotlight or the Applications folder. +Downloads the latest signed DMG, verifies its SHA256 against `checksums.txt`, copies `Zee.app` to `/Applications`, and clears the quarantine attribute. Pin a version with `VERSION=v0.3.0 bash`. -On first launch, macOS may warn that the app can't be verified. Run once to clear it: - -```bash -xattr -cr /Applications/Zee.app -``` - -### macOS (DMG) +### Manual DMG 1. Download `Zee-.dmg` from the [latest release](https://github.com/sumerc/zee/releases/latest) 2. Open the DMG and drag **Zee.app** to **Applications** +3. Clear quarantine: `xattr -cr /Applications/Zee.app` ### CLI binary -For terminal usage, install the formula or download directly: - -```bash -brew install sumerc/tap/zee # installs to /opt/homebrew/bin/zee -``` +For terminal usage: ```bash -# or download manually — Apple Silicon +# Apple Silicon curl -L https://github.com/sumerc/zee/releases/latest/download/zee_darwin_arm64.tar.gz | tar xz # Intel @@ -67,9 +58,9 @@ curl -L https://github.com/sumerc/zee/releases/latest/download/zee_darwin_amd64. ``` ```bash -GROQ_API_KEY=xxx zee # Groq Whisper -OPENAI_API_KEY=xxx zee -stream # Deepgram streaming -zee -debug # with diagnostic logging +GROQ_API_KEY=xxx ./zee # Groq Whisper +DEEPGRAM_API_KEY=xxx ./zee # Deepgram (streaming auto-enabled when a streaming model is selected from the tray) +./zee -debug # with diagnostic logging ``` > **Note:** When running from a terminal, macOS permissions (Microphone, Accessibility) are granted to the **terminal app** (e.g. Ghostty, iTerm2, Terminal), not to zee itself. @@ -92,7 +83,6 @@ export OPENAI_API_KEY=your_key # batch mode (OpenAI Whisper) export DEEPGRAM_API_KEY=your_key # streaming mode (Deepgram) export MISTRAL_API_KEY=your_key # batch mode (Mistral Voxtral) zee # starts in menu bar, hold Ctrl+Shift+Space to record -zee -stream # words appear as you speak ``` > **Note:** `export` only works in the current terminal session. To make API keys available to `Zee.app` when launched from Spotlight or Applications, use `launchctl`: @@ -128,7 +118,6 @@ make benchmark WAV=file.wav RUNS=5 # multiple runs for timing | Flag | Default | Description | |------|---------|-------------| -| `-stream` | false | Real-time streaming transcription (Deepgram) | | `-format` | mp3@16 | Audio format: `mp3@16`, `mp3@64`, or `flac` | | `-autopaste` | true | Auto-paste into focused window | | `-setup` | false | Select microphone device | diff --git a/main.go b/main.go index 0c551d5..6327ae5 100644 --- a/main.go +++ b/main.go @@ -159,7 +159,6 @@ func run() { benchmarkFile := flag.String("benchmark", "", "Run benchmark with WAV file instead of live recording") benchmarkRuns := flag.Int("runs", 3, "Number of benchmark iterations") autoPasteFlag := flag.Bool("autopaste", true, "Auto-paste to focused window after transcription") - streamFlag := flag.Bool("stream", false, "Enable streaming transcription (Deepgram only)") setupFlag := flag.Bool("setup", false, "Select microphone device (otherwise uses system default)") deviceFlag := flag.String("device", "", "Use named microphone device") formatFlag := flag.String("format", "mp3@16", "Audio format: mp3@16, mp3@64, or flac") @@ -238,8 +237,6 @@ func run() { } else { autoPaste = *autoPasteFlag } - streamEnabled = *streamFlag - // Validate format switch *formatFlag { case "mp3@16", "mp3@64", "flac": @@ -251,10 +248,6 @@ func run() { fatal("Unknown format %q (use mp3@16, mp3@64, or flac)", *formatFlag) } - if streamEnabled && *formatFlag != "mp3@16" { - log.Warn("format ignored in streaming mode") - } - // Restore saved provider/model or fall back to auto-detection if cfg.Provider != "" { for _, p := range transcriber.Providers() { diff --git a/test/integration_test.go b/test/integration_test.go index 10d546d..b6cd8df 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -165,14 +165,14 @@ func TestBatchEarlyKeyup(t *testing.T) { func TestStreamWords(t *testing.T) { requireDeepgramKey(t) logDir := runZee(t, cmds("KEYDOWN", "WAIT_AUDIO_DONE", "SLEEP 300", "KEYUP", "WAIT", "QUIT"), - "-test", "-stream", "data/short.wav") + "-test", "data/short.wav") requireTranscription(t, logDir) } func TestStreamMetrics(t *testing.T) { requireDeepgramKey(t) logDir := runZee(t, cmds("KEYDOWN", "WAIT_AUDIO_DONE", "SLEEP 300", "KEYUP", "WAIT", "QUIT"), - "-test", "-stream", "data/short.wav") + "-test", "data/short.wav") diag := readLog(t, logDir, "diagnostics_log.txt") if !strings.Contains(diag, "stream_transcription") { t.Error("expected stream_transcription in diagnostics") @@ -185,7 +185,7 @@ func TestStreamMetrics(t *testing.T) { func TestStreamKeyupAtBoundary(t *testing.T) { requireDeepgramKey(t) logDir := runZee(t, cmds("KEYDOWN", "WAIT_AUDIO_DONE", "KEYUP", "WAIT", "QUIT"), - "-test", "-stream", "data/short.wav") + "-test", "data/short.wav") _ = readLog(t, logDir, "diagnostics_log.txt") } @@ -240,8 +240,8 @@ func TestNoVoiceWarningBatch(t *testing.T) { func TestNoVoiceWarningStream(t *testing.T) { logDir := runZeeOpts(t, cmds("KEYDOWN", silenceWarnSleep, "KEYUP", "WAIT", "QUIT"), - runOpts{env: []string{"ZEE_FAKE_TEXT=hello", "GROQ_API_KEY=", "DEEPGRAM_API_KEY="}}, - "-test", "-stream", "data/silence.wav") + runOpts{env: []string{"ZEE_FAKE_TEXT=hello", "ZEE_FAKE_STREAM=1", "GROQ_API_KEY=", "DEEPGRAM_API_KEY="}}, + "-test", "data/silence.wav") diag := readLog(t, logDir, "diagnostics_log.txt") if !strings.Contains(diag, "no_voice_warning") { t.Errorf("expected 'no_voice_warning' in diagnostics, got: %q", diag) diff --git a/transcriber/fake.go b/transcriber/fake.go index 6b72363..ec69b08 100644 --- a/transcriber/fake.go +++ b/transcriber/fake.go @@ -3,26 +3,38 @@ package transcriber import ( "context" "fmt" + "os" "time" ) type FakeTranscriber struct { - text string - err error - lang string + text string + err error + lang string + stream bool } func NewFake(text string, err error) *FakeTranscriber { - return &FakeTranscriber{text: text, err: err} + return &FakeTranscriber{text: text, err: err, stream: os.Getenv("ZEE_FAKE_STREAM") == "1"} } func (f *FakeTranscriber) Name() string { return "fake" } func (f *FakeTranscriber) SupportedLanguages() []Language { return nil } func (f *FakeTranscriber) SetLanguage(lang string) { f.lang = lang } func (f *FakeTranscriber) GetLanguage() string { return f.lang } -func (f *FakeTranscriber) Models() []ModelInfo { return nil } +func (f *FakeTranscriber) Models() []ModelInfo { + if f.stream { + return []ModelInfo{{ID: "fake-stream", Label: "fake (stream)", Stream: true}} + } + return nil +} func (f *FakeTranscriber) SetModel(_ string) {} -func (f *FakeTranscriber) GetModel() string { return "" } +func (f *FakeTranscriber) GetModel() string { + if f.stream { + return "fake-stream" + } + return "" +} func (f *FakeTranscriber) NewSession(_ context.Context, cfg SessionConfig) (Session, error) { updates := make(chan string, 1) From baa4e8d1405d3d97e850bbb79b27acaa9ee10c9b Mon Sep 17 00:00:00 2001 From: sumerc Date: Thu, 21 May 2026 00:42:33 +0300 Subject: [PATCH 2/5] update few more flags --- main.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/main.go b/main.go index 6327ae5..fffb025 100644 --- a/main.go +++ b/main.go @@ -167,11 +167,9 @@ func run() { debugFlag := flag.Bool("debug", true, "Enable diagnostic logging (timing, errors, events)") debugTranscribeFlag := flag.Bool("debug-transcribe", false, "Enable transcription text logging") langFlag := flag.String("lang", "en", "Language code for transcription (e.g., en, es, fr). Empty = auto-detect") - crashFlag := flag.Bool("crash", false, "Trigger synthetic panic for testing crash logging") logPathFlag := flag.String("logpath", "", "log directory path (default: OS-specific location, use ./ for current dir)") - profileFlag := flag.String("profile", "", "Enable pprof profiling server (e.g., :6060 or localhost:6060)") testFlag := flag.Bool("test", false, "Test mode (headless, stdin-driven)") - longPressFlag := flag.Duration("longpress", 350*time.Millisecond, "Long-press threshold for PTT vs tap (e.g., 350ms)") + longPressDurationFlag := flag.Duration("longpressduration", 350*time.Millisecond, "Long-press threshold for PTT vs tap (e.g., 350ms)") hintsFlag := flag.String("hints", "", "Vocabulary hints for transcription (comma-separated)") transcribeFlag := flag.String("transcribe", "", "Transcribe an audio file and exit") flag.Parse() @@ -194,16 +192,16 @@ func run() { debug.SetCrashOutput(crashFile, debug.CrashOptions{}) } - if *profileFlag != "" { + if pprofAddr := os.Getenv("ZEE_PPROF"); pprofAddr != "" { go func() { - fmt.Fprintf(os.Stderr, "pprof server listening on http://%s/debug/pprof/\n", *profileFlag) - if err := http.ListenAndServe(*profileFlag, nil); err != nil { + fmt.Fprintf(os.Stderr, "pprof server listening on http://%s/debug/pprof/\n", pprofAddr) + if err := http.ListenAndServe(pprofAddr, nil); err != nil { fmt.Fprintf(os.Stderr, "pprof server error: %v\n", err) } }() } - if *crashFlag { + if os.Getenv("ZEE_CRASH") == "1" { panic("TEST CRASH: synthetic panic to verify crash logging") } @@ -551,7 +549,7 @@ func run() { } sessions := make(chan recSession, 1) - go listenHotkey(hk, *longPressFlag, sessions) + go listenHotkey(hk, *longPressDurationFlag, sessions) go func() { for range trayRecordChan { From 2914363c70b60e8f02513da5d9fae52f85a3e3f6 Mon Sep 17 00:00:00 2001 From: sumerc Date: Thu, 21 May 2026 00:44:04 +0300 Subject: [PATCH 3/5] better longpress cfg --- CHANGELOG.md | 1 + main.go | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82442f1..95e1b3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Removed - `-stream` CLI flag — streaming is derived from the selected provider/model (set via tray menu / persisted in `config.json`). The flag was overwritten ~40 lines after being read and had no effect. - Spurious "format ignored in streaming mode" warning. +- `-crash`, `-profile`, and `-longpress` CLI flags — moved to env vars (`ZEE_CRASH=1`, `ZEE_PPROF=:6060`, `ZEE_LONGPRESS_DURATION=350ms`). Dev/tuning knobs, no need to clutter `-h`. ### Added - `ZEE_FAKE_STREAM=1` env var to make the fake transcriber report a streaming model (used by integration tests that previously relied on `-stream`). diff --git a/main.go b/main.go index fffb025..714884c 100644 --- a/main.go +++ b/main.go @@ -169,7 +169,6 @@ func run() { langFlag := flag.String("lang", "en", "Language code for transcription (e.g., en, es, fr). Empty = auto-detect") logPathFlag := flag.String("logpath", "", "log directory path (default: OS-specific location, use ./ for current dir)") testFlag := flag.Bool("test", false, "Test mode (headless, stdin-driven)") - longPressDurationFlag := flag.Duration("longpressduration", 350*time.Millisecond, "Long-press threshold for PTT vs tap (e.g., 350ms)") hintsFlag := flag.String("hints", "", "Vocabulary hints for transcription (comma-separated)") transcribeFlag := flag.String("transcribe", "", "Transcribe an audio file and exit") flag.Parse() @@ -549,7 +548,7 @@ func run() { } sessions := make(chan recSession, 1) - go listenHotkey(hk, *longPressDurationFlag, sessions) + go listenHotkey(hk, longPressDuration(), sessions) go func() { for range trayRecordChan { @@ -574,6 +573,17 @@ func run() { } } +func longPressDuration() time.Duration { + const def = 350 * time.Millisecond + if v := os.Getenv("ZEE_LONGPRESS_DURATION"); v != "" { + if d, err := time.ParseDuration(v); err == nil { + return d + } + log.Warnf("invalid ZEE_LONGPRESS_DURATION %q, using default %s", v, def) + } + return def +} + func listenHotkey(hk hotkey.Hotkey, longPress time.Duration, sessions chan<- recSession) { type state int const ( From e291c50e446d943e5303fd00d4c487ae72071804 Mon Sep 17 00:00:00 2001 From: sumerc Date: Mon, 25 May 2026 22:32:00 +0300 Subject: [PATCH 4/5] simplify install script and add release scripts to Makefile --- Makefile | 37 +++++++++++++++++++----- README.md | 2 +- install.sh | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 8 deletions(-) create mode 100755 install.sh diff --git a/Makefile b/Makefile index 596062d..598f2df 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build build-linux-amd64 build-linux-arm64 test test-integration benchmark integration-test clean release icns app +.PHONY: build build-linux-amd64 build-linux-arm64 test test-integration benchmark integration-test clean bump-version release icns app VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") @@ -40,10 +40,33 @@ app: build icns clean: rm -f zee Zee-*.dmg +bump-version: + @branch=$$(git rev-parse --abbrev-ref HEAD); \ + if [ "$$branch" != "main" ]; then echo "ERROR: must be on main branch" && exit 1; fi; \ + ver="$(VER)"; \ + if [ -z "$$ver" ]; then echo "usage: make bump-version VER=0.3.7" && exit 1; fi; \ + latest=$$(git tag --sort=-v:refname | head -1); \ + claude -p "Look at the git log from tag $$latest to HEAD. Write a CHANGELOG.md entry for Zee version v$$ver in this exact format: ## v$$ver, blank line, then markdown groups using ### Added, ### Changed, ### Fixed, ### Removed only when relevant, with concise '- ' bullets. Skip merge commits and CI-only changes. Output ONLY the changelog entry, no code fences." > /tmp/zee-changelog-entry; \ + echo "" >> /tmp/zee-changelog-entry; \ + sed -i '' '/^## Unreleased/r /tmp/zee-changelog-entry' CHANGELOG.md; \ + rm -f /tmp/zee-changelog-entry; \ + echo "CHANGELOG.md updated — review and edit as needed" + release: - @latest=$$(gh release view --json tagName -q .tagName 2>/dev/null || echo "none"); \ - echo "latest release: $$latest"; \ - read -p "new version (e.g. 0.2.0): " ver; \ - test -n "$$ver" || (echo "aborted" && exit 1); \ - echo "tagging v$$ver and pushing..."; \ - git tag "v$$ver" && git push origin "v$$ver" + @branch=$$(git rev-parse --abbrev-ref HEAD); \ + if [ "$$branch" != "main" ]; then echo "ERROR: must be on main branch" && exit 1; fi; \ + ver="$(VER)"; \ + if [ -z "$$ver" ]; then echo "usage: make release VER=0.3.7" && exit 1; fi; \ + grep -q "^## v$$ver$$" CHANGELOG.md || (echo "ERROR: v$$ver missing from CHANGELOG.md — run make bump-version first" && exit 1); \ + git diff --quiet || (echo "ERROR: working tree has uncommitted changes" && exit 1); \ + git diff --cached --quiet || (echo "ERROR: index has staged changes" && exit 1); \ + notes=$$(awk "/^## v$$ver$$/{found=1; next} /^## /{if(found) exit} found{print}" CHANGELOG.md | sed '/^$$/d'); \ + echo ""; \ + echo "v$$ver Release Notes:"; \ + echo ""; \ + echo "$$notes"; \ + echo ""; \ + read -p "create and push tag v$$ver? [y/N] " confirm; \ + case "$$confirm" in y|Y) ;; *) echo "aborted" && exit 1;; esac; \ + git tag -a "v$$ver" -m "v$$ver"; \ + git push origin "v$$ver" diff --git a/README.md b/README.md index ec97158..ee78eb4 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ curl -fsSL https://raw.githubusercontent.com/sumerc/zee/main/install.sh | bash ``` -Downloads the latest signed DMG, verifies its SHA256 against `checksums.txt`, copies `Zee.app` to `/Applications`, and clears the quarantine attribute. Pin a version with `VERSION=v0.3.0 bash`. +Downloads the latest DMG, verifies its SHA256 against `checksums.txt`, copies `Zee.app` to `/Applications`, and clears the quarantine attribute. Pin a version with `VERSION=vX.Y.Z bash`. ### Manual DMG diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..21d0fe9 --- /dev/null +++ b/install.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Zee installer for macOS. +# Usage: curl -fsSL https://raw.githubusercontent.com/sumerc/zee/main/install.sh | bash +# VERSION=vX.Y.Z bash install.sh +set -euo pipefail + +REPO="sumerc/zee" +APP_DIR="/Applications" +TMP="$(mktemp -d)" +MOUNT="" + +err() { echo "error: $*" >&2; exit 1; } +log() { echo "==> $*"; } +cleanup() { + [[ -n "$MOUNT" ]] && hdiutil detach "$MOUNT" -quiet >/dev/null 2>&1 || true + rm -rf "$TMP" +} +run_or_sudo() { + "$@" 2>/dev/null || { log "Need sudo: $*"; sudo "$@"; } +} +trap cleanup EXIT + +[[ "$(uname -s)" == "Darwin" ]] || err "Zee currently supports macOS only." + +VERSION="${VERSION:-}" +if [[ -z "$VERSION" ]]; then + log "Resolving latest release..." + VERSION="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \ + | awk -F'"' '/"tag_name"/ {print $4; exit}')" + [[ -n "$VERSION" ]] || err "could not resolve latest version (GitHub API rate limit?). Set VERSION=vX.Y.Z and retry." +fi +log "Installing Zee ${VERSION}" + +DMG="Zee-${VERSION}.dmg" +BASE="https://github.com/${REPO}/releases/download/${VERSION}" + +log "Downloading ${DMG}..." +curl -fL --progress-bar "${BASE}/${DMG}" -o "${TMP}/${DMG}" \ + || err "download failed: ${BASE}/${DMG}" + +log "Verifying checksum..." +curl -fsSL "${BASE}/checksums.txt" -o "${TMP}/checksums.txt" \ + || err "download failed: ${BASE}/checksums.txt" +expected="$(awk -v f="${DMG}" '$2==f {print $1}' "${TMP}/checksums.txt")" +[[ -n "$expected" ]] || err "${DMG} not found in checksums.txt" +actual="$(shasum -a 256 "${TMP}/${DMG}" | awk '{print $1}')" +[[ "$expected" == "$actual" ]] || err "checksum mismatch (expected $expected, got $actual)" +log "Checksum OK" + +log "Mounting DMG..." +MOUNT="$(hdiutil attach -nobrowse -readonly -mountrandom /tmp "${TMP}/${DMG}" \ + | grep -oE '/(private/tmp|Volumes)/[^[:space:]]+' \ + | tail -1)" +[[ -n "$MOUNT" && -d "$MOUNT/Zee.app" ]] || err "Zee.app not found in DMG" + +if [[ -d "${APP_DIR}/Zee.app" ]]; then + log "Removing existing ${APP_DIR}/Zee.app" + run_or_sudo rm -rf "${APP_DIR}/Zee.app" +fi + +log "Copying Zee.app to ${APP_DIR}..." +run_or_sudo cp -R "$MOUNT/Zee.app" "${APP_DIR}/" + +log "Clearing quarantine attribute..." +run_or_sudo xattr -cr "${APP_DIR}/Zee.app" + +cat < Date: Mon, 25 May 2026 22:34:45 +0300 Subject: [PATCH 5/5] parallelize int-tests --- .github/workflows/release.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 791c951..34dfaf8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Verify tag is on main run: git merge-base --is-ancestor ${{ github.sha }} origin/main - test: + unit-test: needs: guard runs-on: macos-15 steps: @@ -31,6 +31,16 @@ jobs: - name: Unit tests run: make test + integration-test: + needs: guard + runs-on: macos-15 + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-go@v6 + with: + go-version: '1.24' + - name: Integration tests env: GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} @@ -38,7 +48,7 @@ jobs: run: make test-integration release: - needs: test + needs: [unit-test, integration-test] runs-on: macos-15 steps: - uses: actions/checkout@v5 @@ -68,4 +78,3 @@ jobs: packaging/mkdmg.sh "$universal" "$VERSION" "Zee-${VERSION}.dmg" shasum -a 256 "Zee-${VERSION}.dmg" >> dist/checksums.txt gh release upload "$VERSION" "Zee-${VERSION}.dmg" dist/checksums.txt --clobber -