Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a8698c9
Merge pull request #90 from spencerkit/develop
pallyoung Aug 12, 2026
0fd993d
release: version packages
github-actions[bot] Aug 12, 2026
46e008a
test: follow desktop package version
pallyoung Aug 12, 2026
b0aeff3
Merge pull request #91 from spencerkit/changeset-release/main
pallyoung Aug 12, 2026
8bd4068
ci: support unsigned Windows desktop releases
pallyoung Aug 12, 2026
9e9bc28
Merge pull request #92 from spencerkit/fix/unsigned-desktop-release
pallyoung Aug 12, 2026
2caa606
fix: preserve canonical acceptance timestamps
pallyoung Aug 12, 2026
6b003a3
Merge pull request #93 from spencerkit/fix/desktop-acceptance-timestamp
pallyoung Aug 12, 2026
8805022
fix: invoke acceptance artifact validation
pallyoung Aug 12, 2026
82cef05
Merge pull request #94 from spencerkit/fix/desktop-acceptance-validation
pallyoung Aug 12, 2026
a21a165
fix: allow empty installed acceptance components
pallyoung Aug 12, 2026
9fefd3e
Merge pull request #95 from spencerkit/fix/desktop-empty-components
pallyoung Aug 12, 2026
2fae057
fix: invoke installed acceptance arguments
pallyoung Aug 12, 2026
075a5d9
Merge pull request #96 from spencerkit/fix/desktop-acceptance-separator
pallyoung Aug 12, 2026
9290ab3
fix: read packaged factory runtime manifest
pallyoung Aug 12, 2026
4813486
Merge pull request #97 from spencerkit/fix/desktop-factory-runtime-ev…
pallyoung Aug 12, 2026
fbe671e
fix: await installed acceptance driver exit (#98)
pallyoung Aug 12, 2026
37ee3de
fix CLI subpath dependency validation (#99)
pallyoung Aug 12, 2026
e1e4d12
fix CLI acceptance argument parsing (#100)
pallyoung Aug 12, 2026
75fc454
fix CLI update acceptance activation (#101)
pallyoung Aug 12, 2026
73e8e83
fix CLI legacy update acceptance (#102)
pallyoung Aug 12, 2026
6e945a2
retry npm candidate tag propagation (#103)
pallyoung Aug 12, 2026
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
8 changes: 0 additions & 8 deletions .changeset/desktop-container.md

This file was deleted.

33 changes: 28 additions & 5 deletions .github/workflows/desktop-acceptance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ name: Publish Desktop acceptance

on:
workflow_dispatch:
inputs:
windows_signing:
description: "Sign Windows executables with Authenticode"
required: true
default: true
type: boolean

permissions:
contents: read
Expand Down Expand Up @@ -93,6 +99,7 @@ jobs:
uses: ./.github/workflows/desktop-verify.yml
with:
signed: true
windows_signing: ${{ inputs.windows_signing }}
signing_key_artifact: ${{ needs.prepare.outputs.signing_key_artifact }}
runtime_update_url: ${{ needs.prepare.outputs.runtime_update_url }}
release_tag: ${{ needs.prepare.outputs.release_tag }}
Expand Down Expand Up @@ -171,10 +178,14 @@ jobs:
exit 1
fi
elif grep -q '(HTTP 404)' "${release_error}"; then
windows_signing_note=""
if [[ "${{ inputs.windows_signing }}" != "true" ]]; then
windows_signing_note=" WARNING: Windows executables in this acceptance build are not Authenticode-signed."
fi
gh release create "${RELEASE_TAG}" \
--target "${GITHUB_SHA}" \
--title "Coder Studio Desktop acceptance ${GITHUB_RUN_NUMBER}" \
--notes "Ephemeral signed acceptance channel for ${GITHUB_SHA}. The signing key is test-only; do not promote these assets to production." \
--notes "Ephemeral Runtime-signed acceptance channel for ${GITHUB_SHA}. The Runtime signing key is test-only; do not promote these assets to production.${windows_signing_note}" \
--draft
else
cat "${release_error}" >&2
Expand Down Expand Up @@ -273,7 +284,10 @@ jobs:
$scenario = '${{ matrix.scenario }}'
$hasPreviousDesktop = '${{ needs.prepare.outputs.has_previous_desktop }}' -eq 'true'
$originalChannel = Get-Content (Join-Path $candidate 'desktop-channel.json') -Raw | ConvertFrom-Json
$generatedAt = $originalChannel.generatedAt
$generatedAt = ([DateTimeOffset]$originalChannel.generatedAt).UtcDateTime.ToString(
'yyyy-MM-ddTHH:mm:ss.fffZ',
[Globalization.CultureInfo]::InvariantCulture
)
$scenarioTag = "${{ needs.prepare.outputs.release_tag }}-$scenario"
if ($hasPreviousDesktop -and $scenario -notin @('combined', 'wsl-combined')) {
pnpm desktop:channel -- --directory $candidate --carry-forward-from $previous
Expand All @@ -288,7 +302,7 @@ jobs:
if ($LASTEXITCODE -ne 0) { throw 'Unable to build scenario Desktop channel' }
$env:CODER_STUDIO_RUNTIME_PUBLIC_KEY = Get-Content release/desktop-ci-signing/runtime-public.pem -Raw
$validation = @(
'desktop:artifacts', '--', 'validate',
'desktop:artifacts', 'validate',
'--directory', $candidate,
'--components', 'desktop,win-runtime,wsl-engine,wsl-runtime'
)
Expand Down Expand Up @@ -361,6 +375,10 @@ jobs:
wsl.exe --export Ubuntu "${env:RUNNER_TEMP}\ubuntu-rootfs.tar"
wsl.exe --import $distro "${env:RUNNER_TEMP}\$distro" "${env:RUNNER_TEMP}\ubuntu-rootfs.tar" --version 2
if ($LASTEXITCODE -ne 0) { throw 'Unable to create disposable WSL acceptance distribution' }
wsl.exe -d $distro -u root -- sh -lc "useradd --create-home --shell /bin/bash coderstudio && printf '[boot]\nsystemd=false\n[user]\ndefault=coderstudio\n' > /etc/wsl.conf"
if ($LASTEXITCODE -ne 0) { throw 'Unable to configure the disposable WSL acceptance user' }
wsl.exe --terminate $distro
if ($LASTEXITCODE -ne 0) { throw 'Unable to restart disposable WSL acceptance distribution' }
"name=$distro" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8

- name: Run installed Desktop update scenario
Expand All @@ -375,20 +393,25 @@ jobs:
'-PreviousRuntimeVersion', '${{ steps.scenario.outputs.previous_runtime }}',
'-ExpectedShellVersion', '${{ steps.scenario.outputs.target_shell }}',
'-ExpectedRuntimeVersion', '${{ steps.scenario.outputs.target_runtime }}',
'-ExpectedComponents', '${{ steps.scenario.outputs.components }}',
'-PublicKeyPath', 'release/desktop-ci-signing/runtime-public.pem',
'-Scenario', '${{ matrix.scenario }}',
'-ReportPath', 'release/desktop-installed-report/${{ matrix.scenario }}.json',
'-CommitSha', '${{ github.sha }}',
'-ChannelSignatureDigest', '${{ steps.scenario.outputs.signature_digest }}'
)
if ('${{ steps.scenario.outputs.components }}') {
$arguments += @('-ExpectedComponents', '${{ steps.scenario.outputs.components }}')
}
if ('${{ matrix.scenario }}' -eq 'runtime-health-rollback') {
$arguments += @('-ExpectedRuntimeAfterRestart', '${{ steps.scenario.outputs.previous_runtime }}')
}
if ('${{ matrix.scenario }}' -in @('fresh-wsl', 'wsl', 'wsl-combined')) {
$arguments += @('-WslDistro', '${{ steps.wsl.outputs.name }}')
}
pnpm acceptance:desktop:installed -- @arguments
if ('${{ inputs.windows_signing }}' -ne 'true') {
$arguments += '-SkipAuthenticode'
}
pnpm acceptance:desktop:installed @arguments
if ($LASTEXITCODE -ne 0) { throw 'Installed Desktop acceptance failed' }

- name: Upload installed-upgrade report
Expand Down
39 changes: 30 additions & 9 deletions .github/workflows/desktop-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ on:
required: true
default: false
type: boolean
windows_signing:
description: "Sign Windows executables with Authenticode"
required: true
default: true
type: boolean
desktop_acceptance_run_id:
description: "Workflow run containing the complete installed Desktop acceptance matrix"
required: false
Expand Down Expand Up @@ -181,7 +186,7 @@ jobs:
retention-days: 14

windows-assets:
name: Build signed Windows assets
name: Build Windows assets
needs: prepare
runs-on: windows-latest
timeout-minutes: 60
Expand All @@ -191,8 +196,9 @@ jobs:
env:
CODER_STUDIO_RUNTIME_SIGNING_PRIVATE_KEY: ${{ secrets.DESKTOP_RUNTIME_SIGNING_PRIVATE_KEY }}
CODER_STUDIO_RUNTIME_PUBLIC_KEY: ${{ secrets.DESKTOP_RUNTIME_PUBLIC_KEY }}
CSC_LINK: ${{ secrets.DESKTOP_WINDOWS_CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.DESKTOP_WINDOWS_CSC_KEY_PASSWORD }}
CSC_LINK: ${{ inputs.windows_signing && secrets.DESKTOP_WINDOWS_CSC_LINK || '' }}
CSC_KEY_PASSWORD: ${{ inputs.windows_signing && secrets.DESKTOP_WINDOWS_CSC_KEY_PASSWORD || '' }}
CSC_IDENTITY_AUTO_DISCOVERY: ${{ inputs.windows_signing && 'true' || 'false' }}
CODER_STUDIO_RELEASE_PUBLISHED_AT: ${{ needs.prepare.outputs.published_at }}
CODER_STUDIO_FACTORY_RELEASE_BASE_URL: https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.tag }}/

Expand All @@ -218,7 +224,7 @@ jobs:
if (-not $env:CODER_STUDIO_RUNTIME_SIGNING_PRIVATE_KEY -or -not $env:CODER_STUDIO_RUNTIME_PUBLIC_KEY) {
throw "Desktop Runtime Ed25519 signing secrets are required"
}
if ("${{ needs.prepare.outputs.release_kind }}" -eq "full" -and (-not $env:CSC_LINK -or -not $env:CSC_KEY_PASSWORD)) {
if ("${{ needs.prepare.outputs.release_kind }}" -eq "full" -and "${{ inputs.windows_signing }}" -eq "true" -and (-not $env:CSC_LINK -or -not $env:CSC_KEY_PASSWORD)) {
throw "Windows Authenticode signing secrets are required for a full Desktop release"
}

Expand All @@ -230,7 +236,7 @@ jobs:
pnpm --filter @coder-studio/desktop test
pnpm ci:typecheck

- name: Build signed Windows assets
- name: Build Windows assets
shell: pwsh
run: |
if ("${{ needs.prepare.outputs.release_kind }}" -eq "full") {
Expand All @@ -241,7 +247,7 @@ jobs:
}

- name: Verify Authenticode signatures
if: needs.prepare.outputs.release_kind == 'full'
if: needs.prepare.outputs.release_kind == 'full' && inputs.windows_signing
shell: pwsh
run: |
$metadata = Get-Content release/desktop/latest.yml -Raw
Expand All @@ -258,7 +264,7 @@ jobs:
}
}

- name: Stage signed Windows assets
- name: Stage Windows assets
shell: pwsh
run: |
$components = 'win-runtime'
Expand Down Expand Up @@ -380,10 +386,16 @@ jobs:
- name: Publish immutable prerelease
shell: bash
run: |
release_notes_args=()
if [[ "${{ needs.prepare.outputs.release_kind }}" == "full" && "${{ inputs.windows_signing }}" != "true" ]]; then
release_notes_args+=(--notes "> [!WARNING]
> Windows executables in this release are not Authenticode-signed. Windows may show an Unknown publisher warning. Runtime, WSL Engine, and update-channel manifests remain cryptographically signed.")
fi
gh release create "${{ needs.prepare.outputs.tag }}" \
--target "${GITHUB_SHA}" \
--title "${{ needs.prepare.outputs.title }}" \
--generate-notes \
"${release_notes_args[@]}" \
--prerelease --latest=false \
release/desktop-release-final/*

Expand Down Expand Up @@ -532,6 +544,10 @@ jobs:
wsl.exe --export Ubuntu "${env:RUNNER_TEMP}\ubuntu-rootfs.tar"
wsl.exe --import $distro "${env:RUNNER_TEMP}\$distro" "${env:RUNNER_TEMP}\ubuntu-rootfs.tar" --version 2
if ($LASTEXITCODE -ne 0) { throw 'Unable to create disposable production WSL distribution' }
wsl.exe -d $distro -u root -- sh -lc "useradd --create-home --shell /bin/bash coderstudio && printf '[boot]\nsystemd=false\n[user]\ndefault=coderstudio\n' > /etc/wsl.conf"
if ($LASTEXITCODE -ne 0) { throw 'Unable to configure the disposable production WSL user' }
wsl.exe --terminate $distro
if ($LASTEXITCODE -ne 0) { throw 'Unable to restart disposable production WSL distribution' }
"name=$distro" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8

- name: Run production installed Desktop update
Expand All @@ -546,17 +562,22 @@ jobs:
'-PreviousRuntimeVersion', '${{ steps.identity.outputs.previous_runtime }}',
'-ExpectedShellVersion', '${{ steps.identity.outputs.target_shell }}',
'-ExpectedRuntimeVersion', '${{ steps.identity.outputs.target_runtime }}',
'-ExpectedComponents', '${{ steps.identity.outputs.components }}',
'-PublicKeyPath', 'release/desktop-production-key/runtime-public.pem',
'-Scenario', '${{ steps.identity.outputs.scenario }}',
'-ReportPath', 'release/desktop-production-report/${{ matrix.target }}.json',
'-CommitSha', '${{ github.sha }}',
'-ChannelSignatureDigest', '${{ steps.identity.outputs.signature_digest }}'
)
if ('${{ steps.identity.outputs.components }}') {
$arguments += @('-ExpectedComponents', '${{ steps.identity.outputs.components }}')
}
if ('${{ matrix.target }}' -eq 'wsl') {
$arguments += @('-WslDistro', '${{ steps.wsl.outputs.name }}')
}
pnpm acceptance:desktop:installed -- @arguments
if ('${{ inputs.windows_signing }}' -ne 'true') {
$arguments += '-SkipAuthenticode'
}
pnpm acceptance:desktop:installed @arguments
if ($LASTEXITCODE -ne 0) { throw 'Production installed Desktop acceptance failed' }

- name: Upload production installed-upgrade report
Expand Down
15 changes: 10 additions & 5 deletions .github/workflows/desktop-verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ on:
type: boolean
required: false
default: false
windows_signing:
type: boolean
required: false
default: true
signing_key_artifact:
type: string
required: false
Expand Down Expand Up @@ -114,8 +118,9 @@ jobs:
timeout-minutes: 60
env:
CODER_STUDIO_RELEASE_PUBLISHED_AT: ${{ needs.prepare.outputs.published_at }}
CSC_LINK: ${{ secrets.windows_csc_link }}
CSC_KEY_PASSWORD: ${{ secrets.windows_csc_key_password }}
CSC_LINK: ${{ inputs.windows_signing && secrets.windows_csc_link || '' }}
CSC_KEY_PASSWORD: ${{ inputs.windows_signing && secrets.windows_csc_key_password || '' }}
CSC_IDENTITY_AUTO_DISCOVERY: ${{ inputs.windows_signing && 'true' || 'false' }}
outputs:
artifact_name: ${{ steps.artifact_name.outputs.value }}

Expand Down Expand Up @@ -170,18 +175,18 @@ jobs:
pnpm ci:typecheck

- name: Require acceptance Authenticode credentials
if: inputs.signed
if: inputs.windows_signing
shell: pwsh
run: |
if (-not $env:CSC_LINK -or -not $env:CSC_KEY_PASSWORD) {
throw 'Windows Authenticode signing secrets are required for signed Desktop acceptance'
throw 'Windows Authenticode signing secrets are required when windows_signing is enabled'
}

- name: Build Windows installer and Runtime
run: pnpm dist:desktop

- name: Verify acceptance Authenticode signatures
if: inputs.signed
if: inputs.windows_signing
shell: pwsh
run: |
$metadata = Get-Content release/desktop/latest.yml -Raw
Expand Down
8 changes: 8 additions & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 0.5.7

### Patch Changes

- [#90](https://github.com/spencerkit/coder-studio/pull/90) [`ff6bb92`](https://github.com/spencerkit/coder-studio/commit/ff6bb92c0ca30c61f8ee35c4785dcc44f5c3f647) Thanks [@pallyoung](https://github.com/pallyoung)! - Ship the first unified Coder Studio and Desktop Shell release, including the Electron container,
isolated state locking, the bundled Node Engine, WSL support, and independently signed Product
Runtime updates with startup validation and automatic rollback.

## 0.5.6

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@spencer-kit/coder-studio",
"version": "0.5.6",
"version": "0.5.7",
"type": "module",
"description": "Self-hosted browser workspace for AI coding agents, review, supervision, and cross-device continuation.",
"main": "./src/index.ts",
Expand Down
9 changes: 9 additions & 0 deletions packages/desktop/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# @coder-studio/desktop

## 0.1.1

### Patch Changes

- [#90](https://github.com/spencerkit/coder-studio/pull/90) [`ff6bb92`](https://github.com/spencerkit/coder-studio/commit/ff6bb92c0ca30c61f8ee35c4785dcc44f5c3f647) Thanks [@pallyoung](https://github.com/pallyoung)! - Ship the first unified Coder Studio and Desktop Shell release, including the Electron container,
isolated state locking, the bundled Node Engine, WSL support, and independently signed Product
Runtime updates with startup validation and automatic rollback.
2 changes: 1 addition & 1 deletion packages/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coder-studio/desktop",
"version": "0.1.0",
"version": "0.1.1",
"description": "Native desktop shell for the local-first Coder Studio agentic workspace.",
"author": "Coder Studio",
"license": "MIT",
Expand Down
14 changes: 12 additions & 2 deletions packages/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ function registerIpcHandlers(rootUserDataDir: string): void {

async function handleStartupFailure(error: unknown): Promise<void> {
const details = error instanceof Error ? error.stack || error.message : String(error);
console.error("Unable to start Coder Studio", details);
await environmentActivation.failPending(details).catch(() => undefined);
if (smokeResultPath) {
await finishSmokeTest(
Expand Down Expand Up @@ -617,7 +618,9 @@ async function startApplication(): Promise<void> {
: undefined;
environmentManager = new DesktopEnvironmentManager({
stateStore: environmentStateStore,
discovery: new WslDiscovery(),
discovery: new WslDiscovery({
probeUserShell: process.env.CODER_STUDIO_DESKTOP_ACCEPTANCE !== "1",
}),
shellVersion: app.getVersion(),
nodeVersion: DESKTOP_NODE_VERSION,
runtimeVersion: webRuntime?.manifest.runtimeVersion ?? productVersion,
Expand All @@ -632,7 +635,14 @@ async function startApplication(): Promise<void> {
undefined,
loadChannel: desktopChannelUrl && runtimePublicKey ? loadDesktopChannel : undefined,
nativeRuntimeUpdateAdapter,
onProgress: emitEnvironmentProgress,
onProgress: (progress) => {
emitEnvironmentProgress(progress);
if (process.env.CODER_STUDIO_DESKTOP_ACCEPTANCE === "1") {
console.error(
`[desktop-acceptance:environment] ${new Date().toISOString()} ${JSON.stringify(progress)}`
);
}
},
});
environmentManager.setActiveTarget(activeEnvironmentTarget);
activeSession = session.fromPartition(getEnvironmentPartition(activeEnvironmentTarget));
Expand Down
39 changes: 38 additions & 1 deletion packages/desktop/src/wsl-discovery.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import type { WslCommandRunner } from "./wsl-command.js";
import { WslDiscovery } from "./wsl-discovery.js";
import { createWslProbeScript, WSL_PROBE_SCRIPT, WslDiscovery } from "./wsl-discovery.js";

function result(stdout: string | Buffer, exitCode = 0) {
return {
Expand All @@ -11,6 +11,43 @@ function result(stdout: string | Buffer, exitCode = 0) {
}

describe("WslDiscovery", () => {
it("isolates interactive shell probe output from inherited WSL pipes", () => {
expect(WSL_PROBE_SCRIPT).toContain('probe_file="/tmp/coder-studio-shell-probe-$$"');
expect(WSL_PROBE_SCRIPT).toContain("/usr/bin/timeout --kill-after=1s 5s");
expect(WSL_PROBE_SCRIPT).toContain('>"$probe_file" 2>/dev/null');
expect(WSL_PROBE_SCRIPT).toContain('cat "$probe_file"');
expect(WSL_PROBE_SCRIPT).toContain('rm -f "$probe_file"');
});

it("can skip the optional user shell probe for host-managed acceptance", async () => {
const runner = vi
.fn<WslCommandRunner>()
.mockResolvedValue(
result(
[
"/home/coderstudio",
"/home/coderstudio/.local/share/coder-studio-desktop",
"x86_64",
"6.6.87.2-microsoft-standard-WSL2",
"glibc 2.39",
"false",
"false",
"",
].join("\n")
)
);
const discovery = new WslDiscovery({
runner,
platform: "win32",
probeUserShell: false,
});

await expect(discovery.probe("Acceptance")).resolves.toMatchObject({ supported: true });
const script = runner.mock.calls[0]?.[0].at(-1);
expect(script).toBe(createWslProbeScript(false));
expect(script).not.toContain("__CODER_STUDIO_USER_PATH__");
});

it("decodes Windows UTF-16 distro output", async () => {
const runner = vi
.fn<WslCommandRunner>()
Expand Down
Loading
Loading