Merge pull request #310 from getagentseal/fix/menubar-wake-recovery

Fix menubar wake recovery and release asset selection
This commit is contained in:
AgentSeal 2026-05-11 10:58:33 -07:00 committed by GitHub
commit 2301577e03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 81 additions and 23 deletions

View file

@ -45,7 +45,9 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: CodeBurnMenubar-${{ steps.version.outputs.value }}
path: mac/.build/dist/CodeBurnMenubar-*.zip
path: |
mac/.build/dist/CodeBurnMenubar-${{ steps.version.outputs.value }}.zip
mac/.build/dist/CodeBurnMenubar-${{ steps.version.outputs.value }}.zip.sha256
if-no-files-found: error
- name: Create / update GitHub Release
@ -66,6 +68,6 @@ jobs:
and macOS shows "cannot verify developer", right-click the app in Finder and
pick Open to whitelist it once.
files: |
mac/.build/dist/CodeBurnMenubar-*.zip
mac/.build/dist/CodeBurnMenubar-*.zip.sha256
mac/.build/dist/CodeBurnMenubar-${{ steps.version.outputs.value }}.zip
mac/.build/dist/CodeBurnMenubar-${{ steps.version.outputs.value }}.zip.sha256
fail_on_unmatched_files: true

View file

@ -95,6 +95,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
self?.forceRefreshTask = nil
self?.forceRefreshStartedAt = nil
self?.forceRefreshGeneration &+= 1
self?.store.resetLoadingState()
self?.refreshLoopTask?.cancel()
self?.refreshLoopTask = nil
}
@ -110,9 +111,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
queue: .main
) { [weak self] _ in
Task { @MainActor in
self?.store.resetLoadingState()
self?.forceRefresh()
if self?.refreshLoopTask == nil { self?.startRefreshLoop() }
self?.recoverRefreshPipelineAfterInterruption(resetLoading: true)
}
}
@ -121,7 +120,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor in self?.forceRefresh() }
Task { @MainActor in
self?.recoverRefreshPipelineAfterInterruption(resetLoading: true)
}
}
}
@ -131,10 +132,24 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor in self?.forceRefresh() }
Task { @MainActor in
self?.recoverRefreshPipelineAfterInterruption(resetLoading: false)
}
}
}
private func recoverRefreshPipelineAfterInterruption(resetLoading: Bool) {
if resetLoading {
store.resetLoadingState()
} else {
_ = store.clearStaleLoadingIfNeeded()
}
if refreshLoopTask == nil {
startRefreshLoop()
}
forceRefresh()
}
private func installLaunchAgentIfNeeded() {
let fm = FileManager.default
let agentName = "com.codeburn.refresh.plist"
@ -232,6 +247,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
private func forceRefresh() {
let now = Date()
_ = clearStaleForceRefreshIfNeeded(now: now)
guard forceRefreshTask == nil else { return }
guard now.timeIntervalSince(lastRefreshTime) > 5 else { return }
lastRefreshTime = now
forceRefreshStartedAt = now

View file

@ -46,7 +46,7 @@ final class UpdateChecker {
let (data, _) = try await URLSession.shared.data(for: request)
let release = try JSONDecoder().decode(GitHubRelease.self, from: data)
guard let asset = release.assets.first(where: {
$0.name.hasPrefix("CodeBurnMenubar-") && $0.name.hasSuffix(".zip")
$0.name.hasPrefix("CodeBurnMenubar-v") && $0.name.hasSuffix(".zip")
}) else { return }
let version = asset.name

View file

@ -11,17 +11,28 @@ import { Readable } from 'node:stream'
/// newest tagged release; we filter its assets list for our zipped .app bundle.
const RELEASE_API = 'https://api.github.com/repos/getagentseal/codeburn/releases/latest'
const APP_BUNDLE_NAME = 'CodeBurnMenubar.app'
const ASSET_PATTERN = /^CodeBurnMenubar-.*\.zip$/
const CHECKSUM_PATTERN = /^CodeBurnMenubar-.*\.zip\.sha256$/
const VERSIONED_ASSET_PATTERN = /^CodeBurnMenubar-v.+\.zip$/
const APP_PROCESS_NAME = 'CodeBurnMenubar'
const SUPPORTED_OS = 'darwin'
const MIN_MACOS_MAJOR = 14
export type InstallResult = { installedPath: string; launched: boolean }
type ReleaseAsset = { name: string; browser_download_url: string }
type ReleaseResponse = { tag_name: string; assets: ReleaseAsset[] }
type ResolvedAssets = { zip: ReleaseAsset; checksum: ReleaseAsset | null }
export type ReleaseAsset = { name: string; browser_download_url: string }
export type ReleaseResponse = { tag_name: string; assets: ReleaseAsset[] }
export type ResolvedAssets = { zip: ReleaseAsset; checksum: ReleaseAsset | null }
export function resolveMenubarReleaseAssets(release: ReleaseResponse): ResolvedAssets {
const zip = release.assets.find(a => VERSIONED_ASSET_PATTERN.test(a.name))
if (!zip) {
throw new Error(
`No ${APP_BUNDLE_NAME} versioned zip found in release ${release.tag_name}. ` +
`Check https://github.com/getagentseal/codeburn/releases.`
)
}
const checksum = release.assets.find(a => a.name === `${zip.name}.sha256`) ?? null
return { zip, checksum }
}
function userApplicationsDir(): string {
return join(homedir(), 'Applications')
@ -71,15 +82,7 @@ async function fetchLatestReleaseAssets(): Promise<ResolvedAssets> {
throw new Error(`GitHub release lookup failed: HTTP ${response.status}`)
}
const body = await response.json() as ReleaseResponse
const zip = body.assets.find(a => ASSET_PATTERN.test(a.name))
if (!zip) {
throw new Error(
`No ${APP_BUNDLE_NAME} zip found in release ${body.tag_name}. ` +
`Check https://github.com/getagentseal/codeburn/releases.`
)
}
const checksum = body.assets.find(a => CHECKSUM_PATTERN.test(a.name)) ?? null
return { zip, checksum }
return resolveMenubarReleaseAssets(body)
}
async function verifyChecksum(archivePath: string, checksumUrl: string): Promise<void> {

View file

@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest'
import { resolveMenubarReleaseAssets, type ReleaseResponse } from '../src/menubar-installer.js'
function asset(name: string) {
return { name, browser_download_url: `https://example.test/${name}` }
}
describe('resolveMenubarReleaseAssets', () => {
it('ignores dev zips and pairs the checksum with the versioned zip', () => {
const release: ReleaseResponse = {
tag_name: 'mac-v0.9.8',
assets: [
asset('CodeBurnMenubar-dev.zip'),
asset('CodeBurnMenubar-dev.zip.sha256'),
asset('CodeBurnMenubar-v0.9.8.zip'),
asset('CodeBurnMenubar-v0.9.8.zip.sha256'),
],
}
const resolved = resolveMenubarReleaseAssets(release)
expect(resolved.zip.name).toBe('CodeBurnMenubar-v0.9.8.zip')
expect(resolved.checksum?.name).toBe('CodeBurnMenubar-v0.9.8.zip.sha256')
})
it('fails when a release only contains dev assets', () => {
const release: ReleaseResponse = {
tag_name: 'mac-v0.9.8',
assets: [
asset('CodeBurnMenubar-dev.zip'),
asset('CodeBurnMenubar-dev.zip.sha256'),
],
}
expect(() => resolveMenubarReleaseAssets(release)).toThrow(/versioned zip/)
})
})