diff --git a/bun.lock b/bun.lock index 052c27d07..225c4e31a 100644 --- a/bun.lock +++ b/bun.lock @@ -26,7 +26,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "0.2.14", + "version": "2026.4.28", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -110,7 +110,7 @@ }, "packages/desktop-electron": { "name": "@opencode-ai/desktop-electron", - "version": "0.2.14", + "version": "2026.4.28", "dependencies": { "@opencode-ai/util": "workspace:*", "effect": "catalog:", @@ -175,7 +175,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "0.2.14", + "version": "2026.4.28", "bin": { "opencode": "./bin/opencode", }, @@ -402,7 +402,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "0.2.14", + "version": "2026.4.28", "dependencies": { "@opencode-ai/core": "workspace:*", "zod": "catalog:", diff --git a/packages/app/package.json b/packages/app/package.json index 6309e1294..0719a1df4 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "0.2.14", + "version": "2026.4.28", "description": "", "type": "module", "exports": { diff --git a/packages/app/src/pages/session/session-layout.test.ts b/packages/app/src/pages/session/session-layout.test.ts index 8df36c361..bdfda36db 100644 --- a/packages/app/src/pages/session/session-layout.test.ts +++ b/packages/app/src/pages/session/session-layout.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { createRoot } from "solid-js" -import { createStableLayoutMemo } from "./session-layout" +import { createStableLayoutMemo } from "./stable-layout-memo" describe("createStableLayoutMemo", () => { test("reuses the last value when a disposed memo turns empty", () => { diff --git a/packages/app/src/pages/session/session-layout.ts b/packages/app/src/pages/session/session-layout.ts index 7e938c430..1a99173e0 100644 --- a/packages/app/src/pages/session/session-layout.ts +++ b/packages/app/src/pages/session/session-layout.ts @@ -1,6 +1,7 @@ import { useParams } from "@solidjs/router" import { createMemo } from "solid-js" import { useLayout } from "@/context/layout" +import { createStableLayoutMemo } from "./stable-layout-memo" export const useSessionKey = () => { const params = useParams() @@ -18,20 +19,3 @@ export const useSessionLayout = () => { view: createStableLayoutMemo(() => layout.view(sessionKey)), } } - -export function createStableLayoutMemo(read: () => T) { - let last: { value: T } | undefined - const memo = createMemo(read) - - return () => { - const value = memo() - if (value !== undefined) { - last = { value } - return value - } - - if (last) return last.value - - throw new Error("Stable layout memo read before initialization") - } -} diff --git a/packages/app/src/pages/session/stable-layout-memo.ts b/packages/app/src/pages/session/stable-layout-memo.ts new file mode 100644 index 000000000..9cfc303e1 --- /dev/null +++ b/packages/app/src/pages/session/stable-layout-memo.ts @@ -0,0 +1,18 @@ +import { createMemo } from "solid-js" + +export function createStableLayoutMemo(read: () => T) { + let last: { value: T } | undefined + const memo = createMemo(read) + + return () => { + const value = memo() + if (value !== undefined) { + last = { value } + return value + } + + if (last) return last.value + + throw new Error("Stable layout memo read before initialization") + } +} diff --git a/packages/desktop-electron/electron-builder-app-update.test.ts b/packages/desktop-electron/electron-builder-app-update.test.ts index 94eb43e71..ebd053fa5 100644 --- a/packages/desktop-electron/electron-builder-app-update.test.ts +++ b/packages/desktop-electron/electron-builder-app-update.test.ts @@ -10,7 +10,11 @@ import { serializeAppUpdateConfig } from "./scripts/write-app-update-config" const roots: string[] = [] type AfterPackContext = Parameters, (...args: any[]) => unknown>>[0] -function macAfterPackContext(appOutDir: string, appBundleName: string, electronPlatformName = "darwin"): AfterPackContext { +function macAfterPackContext( + appOutDir: string, + appBundleName: string, + electronPlatformName = "darwin", +): AfterPackContext { return { appOutDir, electronPlatformName, @@ -45,13 +49,19 @@ describe("electron builder app-update config", () => { const config = createConfig("prod") expect(config.productName).toBe("PawWork") expect(config.appId).toBe("ai.pawwork.desktop") - expect(config.artifactName).toBe("pawwork-${os}-${arch}.${ext}") + expect(config.artifactName).toBe("pawwork-${os}-${arch}-${version}.${ext}") expect(config.publish).toMatchObject({ owner: "Astro-Han", repo: "pawwork" }) expect(createConfig("prod").mac?.extendInfo).toMatchObject({ LSHasLocalizedDisplayName: true, }) }) + test("all channels share the versioned artifact name", () => { + expect(createConfig("dev").artifactName).toBe("pawwork-${os}-${arch}-${version}.${ext}") + expect(createConfig("beta").artifactName).toBe("pawwork-${os}-${arch}-${version}.${ext}") + expect(createConfig("prod").artifactName).toBe("pawwork-${os}-${arch}-${version}.${ext}") + }) + test("afterPack writes app-update.yml to the packager-reported macOS resources path", async () => { const root = mkdtempSync(join(tmpdir(), "pawwork-builder-config-")) roots.push(root) diff --git a/packages/desktop-electron/electron-builder.config.ts b/packages/desktop-electron/electron-builder.config.ts index a0f43d591..be2dd3328 100644 --- a/packages/desktop-electron/electron-builder.config.ts +++ b/packages/desktop-electron/electron-builder.config.ts @@ -52,7 +52,7 @@ async function writeLocalizedMacDisplayName(resourcesDir: string, channel: Chann } const getBase = (): Configuration => ({ - artifactName: "pawwork-${os}-${arch}.${ext}", + artifactName: "pawwork-${os}-${arch}-${version}.${ext}", directories: { output: "dist", buildResources: "resources", diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index 71652cee3..d5244486b 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop-electron", "private": true, - "version": "0.2.14", + "version": "2026.4.28", "type": "module", "license": "Apache-2.0", "homepage": "https://pawwork.ai", diff --git a/packages/desktop-electron/scripts/verify-release.test.ts b/packages/desktop-electron/scripts/verify-release.test.ts index ac6e6d9d3..e3a5b7a2f 100644 --- a/packages/desktop-electron/scripts/verify-release.test.ts +++ b/packages/desktop-electron/scripts/verify-release.test.ts @@ -10,6 +10,8 @@ import { normalizeTag, parseUpdaterFileUrls, readStartupLogFile, + releaseAssetNames, + releaseUpdaterAssetNames, verifyReleasePayload, verifyStartupLog, type GithubRelease, @@ -22,41 +24,41 @@ afterEach(() => { }) const baseRelease: GithubRelease = { - tag_name: "v0.2.6", + tag_name: "v2026.4.28", draft: false, prerelease: false, assets: [ { - name: "pawwork-mac-arm64.dmg", - browser_download_url: "https://example.com/pawwork-mac-arm64.dmg", + name: "pawwork-mac-arm64-2026.4.28.dmg", + browser_download_url: "https://example.com/pawwork-mac-arm64-2026.4.28.dmg", }, { - name: "pawwork-mac-arm64.zip", - browser_download_url: "https://example.com/pawwork-mac-arm64.zip", + name: "pawwork-mac-arm64-2026.4.28.zip", + browser_download_url: "https://example.com/pawwork-mac-arm64-2026.4.28.zip", }, { - name: "pawwork-mac-arm64.zip.blockmap", - browser_download_url: "https://example.com/pawwork-mac-arm64.zip.blockmap", + name: "pawwork-mac-arm64-2026.4.28.zip.blockmap", + browser_download_url: "https://example.com/pawwork-mac-arm64-2026.4.28.zip.blockmap", }, { - name: "pawwork-mac-x64.dmg", - browser_download_url: "https://example.com/pawwork-mac-x64.dmg", + name: "pawwork-mac-x64-2026.4.28.dmg", + browser_download_url: "https://example.com/pawwork-mac-x64-2026.4.28.dmg", }, { - name: "pawwork-mac-x64.zip", - browser_download_url: "https://example.com/pawwork-mac-x64.zip", + name: "pawwork-mac-x64-2026.4.28.zip", + browser_download_url: "https://example.com/pawwork-mac-x64-2026.4.28.zip", }, { - name: "pawwork-mac-x64.zip.blockmap", - browser_download_url: "https://example.com/pawwork-mac-x64.zip.blockmap", + name: "pawwork-mac-x64-2026.4.28.zip.blockmap", + browser_download_url: "https://example.com/pawwork-mac-x64-2026.4.28.zip.blockmap", }, { - name: "pawwork-win-x64.exe", - browser_download_url: "https://example.com/pawwork-win-x64.exe", + name: "pawwork-win-x64-2026.4.28.exe", + browser_download_url: "https://example.com/pawwork-win-x64-2026.4.28.exe", }, { - name: "pawwork-win-x64.exe.blockmap", - browser_download_url: "https://example.com/pawwork-win-x64.exe.blockmap", + name: "pawwork-win-x64-2026.4.28.exe.blockmap", + browser_download_url: "https://example.com/pawwork-win-x64-2026.4.28.exe.blockmap", }, { name: "latest.yml", @@ -71,36 +73,62 @@ const baseRelease: GithubRelease = { describe("verify-release", () => { test("normalizes release tags", () => { - expect(normalizeTag("0.2.6")).toBe("v0.2.6") - expect(normalizeTag("v0.2.6")).toBe("v0.2.6") - expect(() => normalizeTag("vv0.2.6")).toThrow("Invalid release tag") + expect(normalizeTag("2026.4.28")).toBe("v2026.4.28") + expect(normalizeTag("v2026.4.28")).toBe("v2026.4.28") + expect(() => normalizeTag("vv2026.4.28")).toThrow("Invalid release tag") expect(() => normalizeTag("")).toThrow("Invalid release tag") expect(() => normalizeTag("v")).toThrow("Invalid release tag") expect(() => normalizeTag("abc")).toThrow("Invalid release tag") + expect(() => normalizeTag("2026.4.28.1")).toThrow("Invalid release tag") + expect(() => normalizeTag("2026.4.28-hotfix.1")).toThrow("Invalid release tag") + }) + + test("derives release and updater asset names from the CalVer version", () => { + expect(releaseAssetNames("2026.4.28")).toEqual([ + "pawwork-mac-arm64-2026.4.28.dmg", + "pawwork-mac-arm64-2026.4.28.zip", + "pawwork-mac-arm64-2026.4.28.zip.blockmap", + "pawwork-mac-x64-2026.4.28.dmg", + "pawwork-mac-x64-2026.4.28.zip", + "pawwork-mac-x64-2026.4.28.zip.blockmap", + "pawwork-win-x64-2026.4.28.exe", + "pawwork-win-x64-2026.4.28.exe.blockmap", + "latest.yml", + "latest-mac.yml", + ]) + expect(releaseUpdaterAssetNames("2026.4.28")).toEqual({ + "latest.yml": ["pawwork-win-x64-2026.4.28.exe"], + "latest-mac.yml": ["pawwork-mac-arm64-2026.4.28.zip", "pawwork-mac-x64-2026.4.28.zip"], + }) }) test("parses updater file urls and path entries", () => { expect( - parseUpdaterFileUrls(`version: 0.2.6 + parseUpdaterFileUrls(`version: 2026.4.28 files: - - url: pawwork-mac-arm64.zip + - url: pawwork-mac-arm64-2026.4.28.zip size: 1 - - url: pawwork-mac-x64.zip + - url: pawwork-mac-x64-2026.4.28.zip size: 2 -path: pawwork-mac-arm64.zip +path: pawwork-mac-arm64-2026.4.28.zip `), - ).toEqual(["pawwork-mac-arm64.zip", "pawwork-mac-x64.zip", "pawwork-mac-arm64.zip"]) + ).toEqual(["pawwork-mac-arm64-2026.4.28.zip", "pawwork-mac-x64-2026.4.28.zip", "pawwork-mac-arm64-2026.4.28.zip"]) }) test("parses quoted updater file urls and path entries", () => { expect( parseUpdaterFileUrls(`files: - - url: "pawwork-mac-arm64.zip" - - url: 'pawwork-mac-x64.zip' # Intel macOS updater asset + - url: "pawwork-mac-arm64-2026.4.28.zip" + - url: 'pawwork-mac-x64-2026.4.28.zip' # Intel macOS updater asset - url: "pawwork-mac#arm64.zip" - path: "pawwork-win-x64.exe" # Windows updater asset + path: "pawwork-win-x64-2026.4.28.exe" # Windows updater asset `), - ).toEqual(["pawwork-mac-arm64.zip", "pawwork-mac-x64.zip", "pawwork-mac#arm64.zip", "pawwork-win-x64.exe"]) + ).toEqual([ + "pawwork-mac-arm64-2026.4.28.zip", + "pawwork-mac-x64-2026.4.28.zip", + "pawwork-mac#arm64.zip", + "pawwork-win-x64-2026.4.28.exe", + ]) }) test("keeps inline comments outside escaped quoted values", () => { @@ -108,17 +136,17 @@ path: pawwork-mac-arm64.zip parseUpdaterFileUrls(String.raw`files: - url: "pawwork-mac\"arm64.zip" # comment - url: "pawwork-mac\\" -path: pawwork-win-x64.exe +path: pawwork-win-x64-2026.4.28.exe `), - ).toEqual([String.raw`pawwork-mac\"arm64.zip`, String.raw`pawwork-mac\\`, "pawwork-win-x64.exe"]) + ).toEqual([String.raw`pawwork-mac\"arm64.zip`, String.raw`pawwork-mac\\`, "pawwork-win-x64-2026.4.28.exe"]) }) test("accepts a stable release with expected assets and updater metadata", () => { expect( verifyReleasePayload({ release: baseRelease, - latestYml: "files:\n - url: pawwork-win-x64.exe\n", - latestMacYml: "files:\n - url: pawwork-mac-arm64.zip\n - url: pawwork-mac-x64.zip\n", + latestYml: "files:\n - url: pawwork-win-x64-2026.4.28.exe\n", + latestMacYml: "files:\n - url: pawwork-mac-arm64-2026.4.28.zip\n - url: pawwork-mac-x64-2026.4.28.zip\n", }), ).toEqual([]) }) @@ -127,9 +155,10 @@ path: pawwork-win-x64.exe expect( verifyReleasePayload({ release: baseRelease, - latestYml: "files:\n - url: https://github.com/Astro-Han/pawwork/releases/download/v0.2.6/pawwork-win-x64.exe\n", + latestYml: + "files:\n - url: https://github.com/Astro-Han/pawwork/releases/download/v2026.4.28/pawwork-win-x64-2026.4.28.exe\n", latestMacYml: - "files:\n - url: https://github.com/Astro-Han/pawwork/releases/download/v0.2.6/pawwork-mac-arm64.zip\n - url: https://github.com/Astro-Han/pawwork/releases/download/v0.2.6/pawwork-mac-x64.zip\n", + "files:\n - url: https://github.com/Astro-Han/pawwork/releases/download/v2026.4.28/pawwork-mac-arm64-2026.4.28.zip\n - url: https://github.com/Astro-Han/pawwork/releases/download/v2026.4.28/pawwork-mac-x64-2026.4.28.zip\n", }), ).toEqual([]) }) @@ -138,10 +167,10 @@ path: pawwork-win-x64.exe expect( verifyReleasePayload({ release: baseRelease, - latestYml: "files:\n - url: pawwork-win-x64.exe\n", - latestMacYml: "files:\n - url: pawwork-mac-x64.zip\n", + latestYml: "files:\n - url: pawwork-win-x64-2026.4.28.exe\n", + latestMacYml: "files:\n - url: pawwork-mac-x64-2026.4.28.zip\n", }), - ).toContain("latest-mac.yml does not include pawwork-mac-arm64.zip") + ).toContain("latest-mac.yml does not include pawwork-mac-arm64-2026.4.28.zip") }) test("reports updater metadata that points to a missing asset", () => { @@ -149,12 +178,12 @@ path: pawwork-win-x64.exe verifyReleasePayload({ release: { ...baseRelease, - assets: baseRelease.assets.filter((asset) => asset.name !== "pawwork-mac-arm64.zip"), + assets: baseRelease.assets.filter((asset) => asset.name !== "pawwork-mac-arm64-2026.4.28.zip"), }, - latestYml: "files:\n - url: pawwork-win-x64.exe\n", - latestMacYml: "files:\n - url: pawwork-mac-arm64.zip\n - url: pawwork-mac-x64.zip\n", + latestYml: "files:\n - url: pawwork-win-x64-2026.4.28.exe\n", + latestMacYml: "files:\n - url: pawwork-mac-arm64-2026.4.28.zip\n - url: pawwork-mac-x64-2026.4.28.zip\n", }), - ).toContain("latest-mac.yml references missing release asset: pawwork-mac-arm64.zip") + ).toContain("latest-mac.yml references missing release asset: pawwork-mac-arm64-2026.4.28.zip") }) test("reports missing installer and updater sidecar assets", () => { @@ -162,15 +191,16 @@ path: pawwork-win-x64.exe release: { ...baseRelease, assets: baseRelease.assets.filter( - (asset) => asset.name !== "pawwork-mac-arm64.dmg" && asset.name !== "pawwork-win-x64.exe.blockmap", + (asset) => + asset.name !== "pawwork-mac-arm64-2026.4.28.dmg" && asset.name !== "pawwork-win-x64-2026.4.28.exe.blockmap", ), }, - latestYml: "files:\n - url: pawwork-win-x64.exe\n", - latestMacYml: "files:\n - url: pawwork-mac-arm64.zip\n - url: pawwork-mac-x64.zip\n", + latestYml: "files:\n - url: pawwork-win-x64-2026.4.28.exe\n", + latestMacYml: "files:\n - url: pawwork-mac-arm64-2026.4.28.zip\n - url: pawwork-mac-x64-2026.4.28.zip\n", }) - expect(failures).toContain("Missing release asset: pawwork-mac-arm64.dmg") - expect(failures).toContain("Missing release asset: pawwork-win-x64.exe.blockmap") + expect(failures).toContain("Missing release asset: pawwork-mac-arm64-2026.4.28.dmg") + expect(failures).toContain("Missing release asset: pawwork-win-x64-2026.4.28.exe.blockmap") }) test("reports missing updater metadata assets without requiring metadata downloads", () => { @@ -185,50 +215,60 @@ path: pawwork-win-x64.exe expect(failures).toContain("Missing release asset: latest.yml") expect(failures).toContain("Missing release asset: latest-mac.yml") - expect(failures).toContain("latest.yml does not include pawwork-win-x64.exe") - expect(failures).toContain("latest-mac.yml does not include pawwork-mac-arm64.zip") - expect(failures).toContain("latest-mac.yml does not include pawwork-mac-x64.zip") + expect(failures).toContain("latest.yml does not include pawwork-win-x64-2026.4.28.exe") + expect(failures).toContain("latest-mac.yml does not include pawwork-mac-arm64-2026.4.28.zip") + expect(failures).toContain("latest-mac.yml does not include pawwork-mac-x64-2026.4.28.zip") }) test("reports draft releases", () => { expect( verifyReleasePayload({ release: { ...baseRelease, draft: true }, - latestYml: "files:\n - url: pawwork-win-x64.exe\n", - latestMacYml: "files:\n - url: pawwork-mac-arm64.zip\n - url: pawwork-mac-x64.zip\n", + latestYml: "files:\n - url: pawwork-win-x64-2026.4.28.exe\n", + latestMacYml: "files:\n - url: pawwork-mac-arm64-2026.4.28.zip\n - url: pawwork-mac-x64-2026.4.28.zip\n", }), - ).toContain("Release v0.2.6 is still a draft") + ).toContain("Release v2026.4.28 is still a draft") }) test("reports prerelease releases", () => { expect( verifyReleasePayload({ release: { ...baseRelease, prerelease: true }, - latestYml: "files:\n - url: pawwork-win-x64.exe\n", - latestMacYml: "files:\n - url: pawwork-mac-arm64.zip\n - url: pawwork-mac-x64.zip\n", + latestYml: "files:\n - url: pawwork-win-x64-2026.4.28.exe\n", + latestMacYml: "files:\n - url: pawwork-mac-arm64-2026.4.28.zip\n - url: pawwork-mac-x64-2026.4.28.zip\n", }), - ).toContain("Release v0.2.6 is marked as a prerelease") + ).toContain("Release v2026.4.28 is marked as a prerelease") }) test("reports malformed updater metadata as missing required updater entries", () => { const failures = verifyReleasePayload({ release: baseRelease, - latestYml: "files:\n - broken: pawwork-win-x64.exe\n", - latestMacYml: "files:\n - broken: pawwork-mac-arm64.zip\n", + latestYml: "files:\n - broken: pawwork-win-x64-2026.4.28.exe\n", + latestMacYml: "files:\n - broken: pawwork-mac-arm64-2026.4.28.zip\n", }) - expect(failures).toContain("latest.yml does not include pawwork-win-x64.exe") - expect(failures).toContain("latest-mac.yml does not include pawwork-mac-arm64.zip") - expect(failures).toContain("latest-mac.yml does not include pawwork-mac-x64.zip") + expect(failures).toContain("latest.yml does not include pawwork-win-x64-2026.4.28.exe") + expect(failures).toContain("latest-mac.yml does not include pawwork-mac-arm64-2026.4.28.zip") + expect(failures).toContain("latest-mac.yml does not include pawwork-mac-x64-2026.4.28.zip") + }) + + test("reports invalid release tags in release payloads without throwing", () => { + expect( + verifyReleasePayload({ + release: { ...baseRelease, tag_name: "v2026.4.28.1" }, + latestYml: "", + latestMacYml: "", + }), + ).toEqual(["Invalid release tag: v2026.4.28.1. Expected vYYYY.M.D or YYYY.M.D."]) }) test("accepts a complete startup log for the release version", () => { expect( verifyReleasePayload({ release: baseRelease, - latestYml: "files:\n - url: pawwork-win-x64.exe\n", - latestMacYml: "files:\n - url: pawwork-mac-arm64.zip\n - url: pawwork-mac-x64.zip\n", - startupLog: `[2026-04-22 21:26:16.088] [info] app starting { version: '0.2.6', packaged: true } + latestYml: "files:\n - url: pawwork-win-x64-2026.4.28.exe\n", + latestMacYml: "files:\n - url: pawwork-mac-arm64-2026.4.28.zip\n - url: pawwork-mac-x64-2026.4.28.zip\n", + startupLog: `[2026-04-22 21:26:16.088] [info] app starting { version: '2026.4.28', packaged: true } [2026-04-22 21:26:18.129] [info] server ready { url: 'http://127.0.0.1:59635' } [2026-04-22 21:26:18.130] [info] loading task finished [2026-04-22 21:26:18.131] [info] init done @@ -241,8 +281,8 @@ path: pawwork-win-x64.exe expect( verifyReleasePayload({ release: baseRelease, - latestYml: "files:\n - url: pawwork-win-x64.exe\n", - latestMacYml: "files:\n - url: pawwork-mac-arm64.zip\n - url: pawwork-mac-x64.zip\n", + latestYml: "files:\n - url: pawwork-win-x64-2026.4.28.exe\n", + latestMacYml: "files:\n - url: pawwork-mac-arm64-2026.4.28.zip\n - url: pawwork-mac-x64-2026.4.28.zip\n", startupLog: "", }), ).toEqual(["Latest startup log does not include any app starting entry"]) @@ -251,9 +291,9 @@ path: pawwork-win-x64.exe test("reports a fresh startup log stuck after sidecar readiness", () => { const failures = verifyReleasePayload({ release: baseRelease, - latestYml: "files:\n - url: pawwork-win-x64.exe\n", - latestMacYml: "files:\n - url: pawwork-mac-arm64.zip\n - url: pawwork-mac-x64.zip\n", - startupLog: `[2026-04-22 21:26:16.088] [info] app starting { version: '0.2.6', packaged: true } + latestYml: "files:\n - url: pawwork-win-x64-2026.4.28.exe\n", + latestMacYml: "files:\n - url: pawwork-mac-arm64-2026.4.28.zip\n - url: pawwork-mac-x64-2026.4.28.zip\n", + startupLog: `[2026-04-22 21:26:16.088] [info] app starting { version: '2026.4.28', packaged: true } [2026-04-22 21:26:16.300] [info] spawning sidecar { url: 'http://127.0.0.1:59635' } [2026-04-22 21:26:16.767] [info] sidecar connection started { url: 'http://127.0.0.1:59635' } [2026-04-22 21:26:18.129] [info] awaiting server ready @@ -269,9 +309,9 @@ path: pawwork-win-x64.exe test("does not accept awaiting server ready as server ready", () => { const failures = verifyReleasePayload({ release: baseRelease, - latestYml: "files:\n - url: pawwork-win-x64.exe\n", - latestMacYml: "files:\n - url: pawwork-mac-arm64.zip\n - url: pawwork-mac-x64.zip\n", - startupLog: `[2026-04-22 21:26:16.088] [info] app starting { version: '0.2.6', packaged: true } + latestYml: "files:\n - url: pawwork-win-x64-2026.4.28.exe\n", + latestMacYml: "files:\n - url: pawwork-mac-arm64-2026.4.28.zip\n - url: pawwork-mac-x64-2026.4.28.zip\n", + startupLog: `[2026-04-22 21:26:16.088] [info] app starting { version: '2026.4.28', packaged: true } [2026-04-22 21:26:18.129] [info] awaiting server ready [2026-04-22 21:26:18.130] [info] loading task finished [2026-04-22 21:26:18.131] [info] init done @@ -284,12 +324,12 @@ path: pawwork-win-x64.exe test("checks the latest startup attempt instead of an older successful launch", () => { const failures = verifyReleasePayload({ release: baseRelease, - latestYml: "files:\n - url: pawwork-win-x64.exe\n", - latestMacYml: "files:\n - url: pawwork-mac-arm64.zip\n - url: pawwork-mac-x64.zip\n", - startupLog: `[2026-04-22 20:00:00.000] [info] app starting { version: '0.2.6', packaged: true } + latestYml: "files:\n - url: pawwork-win-x64-2026.4.28.exe\n", + latestMacYml: "files:\n - url: pawwork-mac-arm64-2026.4.28.zip\n - url: pawwork-mac-x64-2026.4.28.zip\n", + startupLog: `[2026-04-22 20:00:00.000] [info] app starting { version: '2026.4.28', packaged: true } [2026-04-22 20:00:01.000] [info] loading task finished [2026-04-22 20:00:01.001] [info] init done -[2026-04-22 21:26:16.088] [info] app starting { version: '0.2.6', packaged: true } +[2026-04-22 21:26:16.088] [info] app starting { version: '2026.4.28', packaged: true } [2026-04-22 21:26:16.767] [info] sidecar connection started { url: 'http://127.0.0.1:59635' } [2026-04-22 21:26:18.129] [info] server ready { url: 'http://127.0.0.1:59635' } `, @@ -303,8 +343,8 @@ path: pawwork-win-x64.exe test("reports release version mismatches in the startup log", () => { const failures = verifyReleasePayload({ release: baseRelease, - latestYml: "files:\n - url: pawwork-win-x64.exe\n", - latestMacYml: "files:\n - url: pawwork-mac-arm64.zip\n - url: pawwork-mac-x64.zip\n", + latestYml: "files:\n - url: pawwork-win-x64-2026.4.28.exe\n", + latestMacYml: "files:\n - url: pawwork-mac-arm64-2026.4.28.zip\n - url: pawwork-mac-x64-2026.4.28.zip\n", startupLog: `[2026-04-22 21:26:16.088] [info] app starting { version: '0.2.5', packaged: true } [2026-04-22 21:26:18.129] [info] server ready { url: 'http://127.0.0.1:59635' } [2026-04-22 21:26:18.130] [info] loading task finished @@ -312,15 +352,15 @@ path: pawwork-win-x64.exe `, }) - expect(failures).toContain("Latest startup log version does not match expected 0.2.6") + expect(failures).toContain("Latest startup log version does not match expected 2026.4.28") }) test("reports startup logs from unpackaged desktop runs", () => { const failures = verifyReleasePayload({ release: baseRelease, - latestYml: "files:\n - url: pawwork-win-x64.exe\n", - latestMacYml: "files:\n - url: pawwork-mac-arm64.zip\n - url: pawwork-mac-x64.zip\n", - startupLog: `[2026-04-22 21:26:16.088] [info] app starting { version: '0.2.6', packaged: false } + latestYml: "files:\n - url: pawwork-win-x64-2026.4.28.exe\n", + latestMacYml: "files:\n - url: pawwork-mac-arm64-2026.4.28.zip\n - url: pawwork-mac-x64-2026.4.28.zip\n", + startupLog: `[2026-04-22 21:26:16.088] [info] app starting { version: '2026.4.28', packaged: false } [2026-04-22 21:26:18.129] [info] server ready { url: 'http://127.0.0.1:59635' } [2026-04-22 21:26:18.130] [info] loading task finished [2026-04-22 21:26:18.131] [info] init done @@ -333,25 +373,25 @@ path: pawwork-win-x64.exe test("reports invalid release tags during startup log verification", () => { expect( verifyStartupLog( - `[2026-04-22 21:26:16.088] [info] app starting { version: '0.2.6', packaged: true } + `[2026-04-22 21:26:16.088] [info] app starting { version: '2026.4.28', packaged: true } [2026-04-22 21:26:18.129] [info] server ready { url: 'http://127.0.0.1:59635' } [2026-04-22 21:26:18.130] [info] loading task finished [2026-04-22 21:26:18.131] [info] init done `, "v", ), - ).toEqual(["Invalid release tag: v. Expected vX.Y.Z or X.Y.Z."]) + ).toEqual(["Invalid release tag: v. Expected vYYYY.M.D or YYYY.M.D."]) }) test("reports invalid release tags with other startup failures", () => { expect( verifyStartupLog( - `[2026-04-22 21:26:16.088] [info] app starting { version: '0.2.6', packaged: true } + `[2026-04-22 21:26:16.088] [info] app starting { version: '2026.4.28', packaged: true } `, "v", ), ).toEqual([ - "Invalid release tag: v. Expected vX.Y.Z or X.Y.Z.", + "Invalid release tag: v. Expected vYYYY.M.D or YYYY.M.D.", "Latest startup log does not include server ready", "Latest startup log does not include loading task finished", "Latest startup log does not include init step done", @@ -361,9 +401,9 @@ path: pawwork-win-x64.exe test("does not accept 'phase: done' in a non-init-step log line", () => { const failures = verifyReleasePayload({ release: baseRelease, - latestYml: "files:\n - url: pawwork-win-x64.exe\n", - latestMacYml: "files:\n - url: pawwork-mac-arm64.zip\n - url: pawwork-mac-x64.zip\n", - startupLog: `[2026-04-22 21:26:16.088] [info] app starting { version: '0.2.6', packaged: true } + latestYml: "files:\n - url: pawwork-win-x64-2026.4.28.exe\n", + latestMacYml: "files:\n - url: pawwork-mac-arm64-2026.4.28.zip\n - url: pawwork-mac-x64-2026.4.28.zip\n", + startupLog: `[2026-04-22 21:26:16.088] [info] app starting { version: '2026.4.28', packaged: true } [2026-04-22 21:26:18.129] [info] server ready { url: 'http://127.0.0.1:59635' } [2026-04-22 21:26:18.130] [info] loading task finished [2026-04-22 21:26:18.131] [info] init step { step: { phase: 'loading' } } @@ -377,9 +417,9 @@ path: pawwork-win-x64.exe test("does not accept legacy init step done without the dedicated marker", () => { const failures = verifyReleasePayload({ release: baseRelease, - latestYml: "files:\n - url: pawwork-win-x64.exe\n", - latestMacYml: "files:\n - url: pawwork-mac-arm64.zip\n - url: pawwork-mac-x64.zip\n", - startupLog: `[2026-04-22 21:26:16.088] [info] app starting { version: '0.2.6', packaged: true } + latestYml: "files:\n - url: pawwork-win-x64-2026.4.28.exe\n", + latestMacYml: "files:\n - url: pawwork-mac-arm64-2026.4.28.zip\n - url: pawwork-mac-x64-2026.4.28.zip\n", + startupLog: `[2026-04-22 21:26:16.088] [info] app starting { version: '2026.4.28', packaged: true } [2026-04-22 21:26:18.129] [info] server ready { url: 'http://127.0.0.1:59635' } [2026-04-22 21:26:18.130] [info] loading task finished [2026-04-22 21:26:18.131] [info] init step { step: { phase: 'done' } } diff --git a/packages/desktop-electron/scripts/verify-release.ts b/packages/desktop-electron/scripts/verify-release.ts index a2bce433d..846f94be9 100644 --- a/packages/desktop-electron/scripts/verify-release.ts +++ b/packages/desktop-electron/scripts/verify-release.ts @@ -21,18 +21,42 @@ type VerificationInput = { const DEFAULT_REPO = "Astro-Han/pawwork" const FETCH_TIMEOUT_MS = 15_000 -const REQUIRED_ASSETS = [ - "pawwork-mac-arm64.dmg", - "pawwork-mac-arm64.zip", - "pawwork-mac-arm64.zip.blockmap", - "pawwork-mac-x64.dmg", - "pawwork-mac-x64.zip", - "pawwork-mac-x64.zip.blockmap", - "pawwork-win-x64.exe", - "pawwork-win-x64.exe.blockmap", - "latest.yml", - "latest-mac.yml", -] +const RELEASE_TARGETS = [ + { os: "mac", arch: "arm64", installerExt: "dmg", updaterExt: "zip", metadata: "latest-mac.yml" }, + { os: "mac", arch: "x64", installerExt: "dmg", updaterExt: "zip", metadata: "latest-mac.yml" }, + { os: "win", arch: "x64", installerExt: "exe", updaterExt: "exe", metadata: "latest.yml" }, +] as const + +type MetadataFile = (typeof RELEASE_TARGETS)[number]["metadata"] + +function releaseTargetAssetName(target: (typeof RELEASE_TARGETS)[number], version: string, ext: string) { + return `pawwork-${target.os}-${target.arch}-${version}.${ext}` +} + +export function releaseAssetNames(version: string) { + return [ + ...new Set([ + ...RELEASE_TARGETS.flatMap((target) => [ + releaseTargetAssetName(target, version, target.installerExt), + releaseTargetAssetName(target, version, target.updaterExt), + `${releaseTargetAssetName(target, version, target.updaterExt)}.blockmap`, + ]), + "latest.yml", + "latest-mac.yml", + ]), + ] +} + +export function releaseUpdaterAssetNames(version: string): Record { + return { + "latest.yml": RELEASE_TARGETS.filter((target) => target.metadata === "latest.yml").map((target) => + releaseTargetAssetName(target, version, target.updaterExt), + ), + "latest-mac.yml": RELEASE_TARGETS.filter((target) => target.metadata === "latest-mac.yml").map((target) => + releaseTargetAssetName(target, version, target.updaterExt), + ), + } +} export function parseUpdaterFileUrls(source: string) { const urls: string[] = [] @@ -67,7 +91,7 @@ function stripInlineComment(value: string) { for (let index = 0; index < value.length; index += 1) { const char = value[index] if ((char === `"` || char === `'`) && !isEscaped(value, index)) { - quote = quote === char ? undefined : quote ?? char + quote = quote === char ? undefined : (quote ?? char) continue } @@ -161,7 +185,8 @@ export function verifyStartupLog(source: string, expectedTag: string) { } if (!hasPackagedStartup(startupLine)) failures.push("Latest startup log does not include packaged true") if (!hasServerReady(latest)) failures.push("Latest startup log does not include server ready") - if (!latest.includes("loading task finished")) failures.push("Latest startup log does not include loading task finished") + if (!latest.includes("loading task finished")) + failures.push("Latest startup log does not include loading task finished") if (!hasInitDone(latest)) failures.push("Latest startup log does not include init step done") return failures @@ -170,24 +195,33 @@ export function verifyStartupLog(source: string, expectedTag: string) { export function verifyReleasePayload(input: VerificationInput) { const failures: string[] = [] const assetNames = new Set(input.release.assets.map((asset) => asset.name)) + let version: string | undefined + try { + version = releaseVersion(input.release.tag_name) + } catch (error) { + failures.push(error instanceof Error ? error.message : String(error)) + } if (input.release.draft) failures.push(`Release ${input.release.tag_name} is still a draft`) if (input.release.prerelease) failures.push(`Release ${input.release.tag_name} is marked as a prerelease`) - for (const asset of REQUIRED_ASSETS) { - if (!assetNames.has(asset)) failures.push(`Missing release asset: ${asset}`) - } - const latestUrls = input.latestYml === undefined ? [] : parseUpdaterFileUrls(input.latestYml) verifyReferencedAssets("latest.yml", latestUrls, assetNames, failures) - if (!hasUpdaterEntry(latestUrls, "pawwork-win-x64.exe")) { - failures.push("latest.yml does not include pawwork-win-x64.exe") - } - const latestMacUrls = input.latestMacYml === undefined ? [] : parseUpdaterFileUrls(input.latestMacYml) verifyReferencedAssets("latest-mac.yml", latestMacUrls, assetNames, failures) - for (const asset of ["pawwork-mac-arm64.zip", "pawwork-mac-x64.zip"]) { - if (!hasUpdaterEntry(latestMacUrls, asset)) failures.push(`latest-mac.yml does not include ${asset}`) + + if (version) { + for (const asset of releaseAssetNames(version)) { + if (!assetNames.has(asset)) failures.push(`Missing release asset: ${asset}`) + } + + const updaterAssets = releaseUpdaterAssetNames(version) + for (const asset of updaterAssets["latest.yml"]) { + if (!hasUpdaterEntry(latestUrls, asset)) failures.push(`latest.yml does not include ${asset}`) + } + for (const asset of updaterAssets["latest-mac.yml"]) { + if (!hasUpdaterEntry(latestMacUrls, asset)) failures.push(`latest-mac.yml does not include ${asset}`) + } } if (input.startupLog !== undefined) failures.push(...verifyStartupLog(input.startupLog, input.release.tag_name)) @@ -197,8 +231,8 @@ export function verifyReleasePayload(input: VerificationInput) { export function normalizeTag(raw: string) { const normalized = raw.startsWith("v") ? raw : `v${raw}` - if (!/^v\d+\.\d+\.\d+$/.test(normalized)) { - throw new Error(`Invalid release tag: ${raw}. Expected vX.Y.Z or X.Y.Z.`) + if (!/^v\d{4}\.\d{1,2}\.\d{1,2}$/.test(normalized)) { + throw new Error(`Invalid release tag: ${raw}. Expected vYYYY.M.D or YYYY.M.D.`) } return normalized } diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 008314a65..3efc1fb25 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "0.2.14", + "version": "2026.4.28", "name": "opencode", "type": "module", "license": "Apache-2.0", diff --git a/packages/util/package.json b/packages/util/package.json index 78afa0e03..d03f8f59b 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "0.2.14", + "version": "2026.4.28", "private": true, "type": "module", "license": "Apache-2.0",