Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 89 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,11 @@ on:
required: false
type: string

# Release metadata is a read-modify-write upload to the same latest*.yml asset.
# Queue same target/phase runs across arch to avoid release asset clobbering.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.phase || 'submit' }}-${{ inputs.channel || 'dev' }}-${{ inputs.target || 'macos' }}-${{ inputs.arch || 'arm64' }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ inputs.source_ref || github.ref_name }}-${{ inputs.phase || 'submit' }}-${{ inputs.channel || 'dev' }}-${{ inputs.target || 'macos' }}
cancel-in-progress: false
Comment thread
Astro-Han marked this conversation as resolved.

permissions:
actions: read
Expand Down Expand Up @@ -297,19 +299,62 @@ jobs:
- name: Install dependencies
run: bun install --frozen-lockfile

- name: Read desktop package version
id: package_version
shell: bash
run: |
set -euo pipefail
version=$(node -p "require('./packages/desktop-electron/package.json').version")
echo "version=$version" >> "$GITHUB_OUTPUT"
Comment thread
Astro-Han marked this conversation as resolved.

- name: Build Electron app
if: ${{ inputs.phase != 'finalize' }}
run: bun run build
working-directory: packages/desktop-electron
env:
OPENCODE_CHANNEL: ${{ inputs.channel || 'dev' }}
PAWWORK_FEEDBACK_FORM_URL: ${{ vars.PAWWORK_FEEDBACK_FORM_URL || '' }}

- name: Setup Apple API Key
if: runner.os == 'macOS'
run: printenv APPLE_API_KEY_CONTENT > $RUNNER_TEMP/apple-api-key.p8
env:
APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY }}

- name: Download existing updater metadata
Comment thread
Astro-Han marked this conversation as resolved.
if: ${{ (runner.os == 'macOS' && (inputs.phase == 'finalize' || inputs.phase == 'full') && inputs.arch == matrix.arch_label) || (runner.os == 'Windows' && inputs.phase == 'full') }}
shell: bash
run: |
set -euo pipefail

existing_dir="$RUNNER_TEMP/existing-latest-yml"
mkdir -p "$existing_dir"
tag="v${{ steps.package_version.outputs.version }}"

download_or_warn() {
local pattern="$1"
local err="$RUNNER_TEMP/${pattern}.err"
if gh release download "$tag" --pattern "$pattern" --dir "$existing_dir" --repo "$GITHUB_REPOSITORY" 2>"$err"; then
return 0
fi
if grep -qiE 'not found|no assets match|could not find|resource not found' "$err"; then
echo "No existing $pattern found; expected for first release."
return 0
fi
cat "$err" >&2
return 1
}

if [[ "$RUNNER_OS" == "macOS" ]]; then
download_or_warn latest-mac.yml
fi
if [[ "$RUNNER_OS" == "Windows" ]]; then
download_or_warn latest.yml
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Package signed app
if: ${{ runner.os == 'macOS' && (inputs.phase == 'submit' || inputs.phase == 'full') }}
run: npx electron-builder --mac dir --${{ matrix.arch_label }} --publish never --config electron-builder.config.ts
Expand Down Expand Up @@ -625,6 +670,7 @@ jobs:
timeout-minutes: 30
env:
OPENCODE_CHANNEL: ${{ inputs.channel || 'dev' }}
PAWWORK_FEEDBACK_FORM_URL: ${{ vars.PAWWORK_FEEDBACK_FORM_URL || '' }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Verify notarized artifacts
Expand Down Expand Up @@ -731,6 +777,7 @@ jobs:
timeout-minutes: 60
env:
OPENCODE_CHANNEL: ${{ inputs.channel || 'dev' }}
PAWWORK_FEEDBACK_FORM_URL: ${{ vars.PAWWORK_FEEDBACK_FORM_URL || '' }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Upload packaged app artifact
Expand All @@ -744,6 +791,46 @@ jobs:
packages/desktop-electron/dist/*.exe.blockmap
packages/desktop-electron/dist/latest*.yml

- name: Collect updater metadata
if: ${{ (runner.os == 'macOS' && (inputs.phase == 'finalize' || inputs.phase == 'full') && inputs.arch == matrix.arch_label) || (runner.os == 'Windows' && inputs.phase == 'full') }}
shell: bash
run: |
set -euo pipefail

case "${{ matrix.target }}-${{ matrix.arch_label }}" in
macos-arm64)
metadata_dir="$RUNNER_TEMP/latest-yml/latest-yml-aarch64-apple-darwin"
metadata_file="latest-mac.yml"
;;
macos-x64)
metadata_dir="$RUNNER_TEMP/latest-yml/latest-yml-x86_64-apple-darwin"
metadata_file="latest-mac.yml"
;;
windows-x64)
metadata_dir="$RUNNER_TEMP/latest-yml/latest-yml-x86_64-pc-windows-msvc"
metadata_file="latest.yml"
;;
*)
echo "Unsupported updater metadata target: ${{ matrix.target }}-${{ matrix.arch_label }}"
exit 1
;;
esac

mkdir -p "$metadata_dir"
cp "dist/$metadata_file" "$metadata_dir/$metadata_file"
working-directory: packages/desktop-electron

- name: Finalize updater metadata
if: ${{ (runner.os == 'macOS' && (inputs.phase == 'finalize' || inputs.phase == 'full') && inputs.arch == matrix.arch_label) || (runner.os == 'Windows' && inputs.phase == 'full') }}
run: bun ./scripts/finalize-latest-yml.ts
working-directory: packages/desktop-electron
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
EXISTING_LATEST_YML_DIR: ${{ runner.temp }}/existing-latest-yml
LATEST_YML_DIR: ${{ runner.temp }}/latest-yml
OPENCODE_VERSION: ${{ steps.package_version.outputs.version }}

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
if: ${{ runner.os == 'macOS' && (inputs.phase == 'finalize' || inputs.phase == 'full') && inputs.arch == matrix.arch_label }}
with:
Expand Down
31 changes: 30 additions & 1 deletion packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import { Font } from "@opencode-ai/ui/font"
import { Splash } from "@opencode-ai/ui/logo"
import { ThemeProvider } from "@opencode-ai/ui/theme/context"
import { MetaProvider } from "@solidjs/meta"
import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
import { type BaseRouterProps, Navigate, Route, Router, useLocation } from "@solidjs/router"
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
import { type Duration, Effect } from "effect"
import {
type Component,
createEffect,
createMemo,
createResource,
createSignal,
Expand Down Expand Up @@ -44,6 +45,7 @@ import { TerminalProvider } from "@/context/terminal"
import DirectoryLayout from "@/pages/directory-layout"
import Layout from "@/pages/layout"
import { ErrorPage } from "./pages/error"
import { buildDesktopContext, type DesktopContext } from "./utils/desktop-context"
import { useCheckServerHealth } from "./utils/server-health"

const HomeRoute = lazy(() => import("@/pages/home"))
Expand Down Expand Up @@ -77,6 +79,7 @@ declare global {
}
api?: {
setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise<void>
setDesktopContext?: (context: DesktopContext) => Promise<void>
}
}
}
Expand Down Expand Up @@ -122,13 +125,39 @@ function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
return (
<AppShellProviders>
<Suspense fallback={<Loading />}>
<DesktopContextRouteBridge />
{props.appChildren}
{props.children}
</Suspense>
</AppShellProviders>
)
}

function DesktopContextRouteBridge() {
const language = useLanguage()
const location = useLocation()

createEffect(() => {
if (!window.api?.setDesktopContext) return
if (isSessionRoute(location.pathname)) return
void window.api
.setDesktopContext(
buildDesktopContext({
route: `${location.pathname}${location.search}${location.hash}`,
locale: language.locale(),
}),
)
.catch(() => undefined)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return null
}

function isSessionRoute(pathname: string) {
const parts = pathname.split("/").filter(Boolean)
return parts.length >= 2 && parts.length <= 3 && parts[1] === "session"
}

export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
return (
<MetaProvider>
Expand Down
24 changes: 24 additions & 0 deletions packages/app/src/components/settings-general.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,30 @@ export const SettingsGeneral: Component = () => {
void platform
.checkUpdate()
.then((result) => {
if (result.status === "busy") {
showToast({
title: language.t("settings.updates.toast.busy.title"),
description: language.t("settings.updates.toast.busy.description"),
})
return
}

if (result.status === "disabled") {
showToast({
title: language.t("settings.updates.toast.disabled.title"),
description: language.t("settings.updates.toast.disabled.description"),
})
return
}

if (result.status === "failed") {
showToast({
title: language.t("common.requestFailed"),
description: result.message ?? language.t("settings.updates.toast.failed.description"),
})
return
}

if (!result.updateAvailable) {
showToast({
variant: "success",
Expand Down
6 changes: 5 additions & 1 deletion packages/app/src/context/platform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ type PickerPaths = string | string[] | null
type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean }
type OpenFilePickerOptions = { title?: string; multiple?: boolean; accept?: string[]; extensions?: string[] }
type SaveFilePickerOptions = { title?: string; defaultPath?: string }
type UpdateInfo = { updateAvailable: boolean; version?: string }
type UpdateFailureReason = "check" | "download" | "metadata"
export type UpdateInfo =
| { updateAvailable: false; status: "disabled" | "none" | "busy"; version?: undefined }
| { updateAvailable: true; status: "ready"; version: string }
| { updateAvailable: false; status: "failed"; reason: UpdateFailureReason; message: string; version?: undefined }

export type Platform = {
/** Platform discriminator */
Expand Down
9 changes: 9 additions & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,10 @@ export const dict = {
"error.page.action.checking": "Checking...",
"error.page.action.checkUpdates": "Check for updates",
"error.page.action.updateTo": "Update to {{version}}",
"error.page.action.upToDate": "PawWork is up to date.",
"error.page.action.busy": "PawWork is already checking for updates.",
"error.page.action.checkFailed": "Failed to check for updates.",
"error.page.action.disabled": "Updates are not available in this build.",
"error.page.circular": "[Circular]",
"error.page.report.prefix": "Please report this error to the PawWork team",
"error.page.report.github": "on GitHub",
Expand Down Expand Up @@ -796,6 +800,11 @@ export const dict = {
"settings.updates.row.check.description": "Manually check for updates and install if available",
"settings.updates.action.checkNow": "Check now",
"settings.updates.action.checking": "Checking...",
"settings.updates.toast.busy.title": "Update check in progress",
"settings.updates.toast.busy.description": "PawWork is already checking for updates.",
"settings.updates.toast.disabled.title": "Updates unavailable",
"settings.updates.toast.disabled.description": "Updates are not available in this build.",
"settings.updates.toast.failed.description": "Failed to check for updates.",
"settings.updates.toast.latest.title": "You're up to date",
"settings.updates.toast.latest.description": "You're running the latest version of PawWork.",
"sound.option.none": "None",
Expand Down
9 changes: 9 additions & 0 deletions packages/app/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,10 @@ export const dict = {
"error.page.action.checking": "检查中...",
"error.page.action.checkUpdates": "检查更新",
"error.page.action.updateTo": "更新到 {{version}}",
"error.page.action.upToDate": "PawWork 已是最新版本。",
"error.page.action.busy": "PawWork 正在检查更新。",
"error.page.action.checkFailed": "检查更新失败。",
"error.page.action.disabled": "此构建不支持更新。",
"error.page.report.prefix": "请将此错误报告给 PawWork 团队",
"error.page.report.github": "在 GitHub 上",
"error.page.version": "版本:{{version}}",
Expand Down Expand Up @@ -692,6 +696,11 @@ export const dict = {
"settings.updates.row.check.description": "手动检查更新并在有更新时安装",
"settings.updates.action.checkNow": "立即检查",
"settings.updates.action.checking": "正在检查...",
"settings.updates.toast.busy.title": "正在检查更新",
"settings.updates.toast.busy.description": "PawWork 已经在检查更新。",
"settings.updates.toast.disabled.title": "更新不可用",
"settings.updates.toast.disabled.description": "此构建不支持更新。",
"settings.updates.toast.failed.description": "检查更新失败。",
"settings.updates.toast.latest.title": "已是最新版本",
"settings.updates.toast.latest.description": "你正在使用最新版本的 PawWork。",

Expand Down
65 changes: 65 additions & 0 deletions packages/app/src/pages/error-update.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, expect, test } from "bun:test"
import { updateErrorPageState } from "./error-update"

const t = (key: string, vars?: Record<string, string | number | boolean>) => {
if (key === "error.page.action.upToDate") return "PawWork is up to date."
if (key === "error.page.action.busy") return "PawWork is already checking for updates."
if (key === "error.page.action.checkFailed") return "Failed to check for updates."
if (key === "error.page.action.disabled") return "Updates are not available in this build."
if (key === "error.page.action.updateTo") return `Update to ${vars?.version ?? ""}`
return key
}

describe("error page update state", () => {
test("shows available update version", () => {
expect(updateErrorPageState({ updateAvailable: true, version: "0.2.5", status: "ready" }, t)).toEqual({
version: "0.2.5",
actionError: undefined,
actionMessage: undefined,
})
})

test("shows no-update feedback", () => {
expect(updateErrorPageState({ updateAvailable: false, status: "none" }, t)).toEqual({
version: undefined,
actionError: undefined,
actionMessage: "PawWork is up to date.",
})
})

test("shows busy feedback", () => {
expect(updateErrorPageState({ updateAvailable: false, status: "busy" }, t)).toEqual({
version: undefined,
actionError: undefined,
actionMessage: "PawWork is already checking for updates.",
})
})

test("shows failed feedback", () => {
expect(
updateErrorPageState({ updateAvailable: false, status: "failed", reason: "check", message: "network down" }, t),
).toEqual({
version: undefined,
actionError: "network down",
actionMessage: undefined,
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

test("falls back to generic failed feedback when message is empty", () => {
expect(
updateErrorPageState({ updateAvailable: false, status: "failed", reason: "check", message: "" }, t),
).toEqual({
version: undefined,
actionError: "Failed to check for updates.",
actionMessage: undefined,
})
})

test("shows disabled feedback", () => {
expect(updateErrorPageState({ updateAvailable: false, status: "disabled" }, t)).toEqual({
version: undefined,
actionError: undefined,
actionMessage: "Updates are not available in this build.",
})
})
})
Loading
Loading