Skip to content
Merged
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
101 changes: 99 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ on:
- master
tags:
- "v*"
# Validate release-pipeline changes before merge. Publishing remains tag-only.
pull_request:
paths:
- .github/workflows/publish.yml
- build.rs
- npm/**
- script/npm-packages.mjs

permissions:
contents: read
Expand Down Expand Up @@ -133,8 +140,98 @@ jobs:
path: dist/*.tar.gz
if-no-files-found: error

build-windows:
needs: checks
strategy:
fail-fast: false
matrix:
include:
- runner: windows-2025
target: x86_64-pc-windows-msvc
arch: x64
- runner: windows-11-arm
target: aarch64-pc-windows-msvc
arch: arm64
runs-on: ${{ matrix.runner }}
timeout-minutes: 45
defaults:
run:
shell: pwsh
env:
CARGO_BUILD_TARGET: ${{ matrix.target }}
RUSTUP_TOOLCHAIN: 1.90.0-x86_64-pc-windows-msvc
# Portable CPU for Ghostty (see build.rs) and no Visual C++ runtime dependency.
OPENCODE_ZIG_CPU: baseline
RUSTFLAGS: -C target-feature=+crt-static
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Enable long dependency paths
run: git config --global core.longpaths true
- uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f # v2.0.5
if: matrix.arch == 'x64'
with:
version: ${{ env.ZIG_VERSION }}
cache-key: release-${{ matrix.target }}
# Zig 0.16 crashes when building Ghostty on the native ARM64 host, so use
# the x64-hosted compiler there (matching windows.yml).
- name: Install x64 Zig for ARM64 cross-compilation
if: matrix.arch == 'arm64'
run: |
$archive = Join-Path $env:RUNNER_TEMP 'zig-x64.zip'
Invoke-WebRequest 'https://ziglang.org/download/0.16.0/zig-x86_64-windows-0.16.0.zip' -OutFile $archive
if ((Get-FileHash $archive -Algorithm SHA256).Hash -ne '68659eb5f1e4eb1437a722f1dd889c5a322c9954607f5edcf337bc3684a75a7e') { throw 'Zig archive checksum mismatch' }
Expand-Archive $archive -DestinationPath $env:RUNNER_TEMP
Join-Path $env:RUNNER_TEMP 'zig-x86_64-windows-0.16.0' >> $env:GITHUB_PATH
- name: Fix Zig 0.16 ARM64 function-pointer alignment
if: matrix.arch == 'arm64'
run: |
$file = Join-Path $env:RUNNER_TEMP 'zig-x86_64-windows-0.16.0/lib/std/debug/SelfInfo/Windows.zig'
$source = [System.IO.File]::ReadAllText($file)
$old = '@ptrCast(try si.loadNtdllProc(@tagName(proc)))'
if (!$source.Contains($old)) { throw 'Zig alignment workaround no longer matches; re-evaluate it' }
[System.IO.File]::WriteAllText($file, $source.Replace($old, '@ptrCast(@alignCast(try si.loadNtdllProc(@tagName(proc))))'))
- name: Install Rust toolchain and target
run: rustup toolchain install $env:RUSTUP_TOOLCHAIN --force-non-host --profile minimal --target $env:CARGO_BUILD_TARGET
- name: Build
run: cargo build --locked --release
- name: Smoke test
run: |
$metadata = cargo metadata --locked --no-deps --format-version 1 | ConvertFrom-Json
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$version = ($metadata.packages | Where-Object name -eq 'opencode-pty').version
$binary = "target/$env:CARGO_BUILD_TARGET/release/opencode-pty.exe"
$output = & $binary --version
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
if ($output -ne "opencode-pty $version (protocol $env:PROTOCOL_VERSION)") { throw "Unexpected version output: $output" }
# Start a real daemon over a named pipe; it stops when nobody claims it.
$runtime = Join-Path $env:RUNNER_TEMP 'opencode-pty-release-smoke'
$daemon = Start-Process $binary -ArgumentList @('daemon', '--runtime-dir', $runtime, '--name', 'smoke') -PassThru -NoNewWindow
$deadline = (Get-Date).AddSeconds(10)
while (!(Test-Path (Join-Path $runtime 'smoke/service.json'))) {
if ($daemon.HasExited -or (Get-Date) -gt $deadline) { throw 'daemon did not register' }
Start-Sleep -Milliseconds 50
}
if (!$daemon.WaitForExit(15000)) { $daemon.Kill(); throw 'unclaimed daemon did not stop' }
if (Test-Path (Join-Path $runtime 'smoke')) { throw 'runtime directory was not removed' }
- name: Package
run: |
$metadata = cargo metadata --locked --no-deps --format-version 1 | ConvertFrom-Json
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$version = ($metadata.packages | Where-Object name -eq 'opencode-pty').version
$name = "opencode-pty-$version-$env:CARGO_BUILD_TARGET"
New-Item -ItemType Directory -Force "dist/$name" | Out-Null
Copy-Item "target/$env:CARGO_BUILD_TARGET/release/opencode-pty.exe" "dist/$name/"
Copy-Item LICENSE, README.md "dist/$name/"
tar -C dist -czf "dist/$name.tar.gz" $name
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: opencode-pty-${{ matrix.target }}
path: dist/*.tar.gz
if-no-files-found: error

release:
needs: build
needs: [build, build-windows]
runs-on: ubuntu-22.04
permissions:
contents: write
Expand All @@ -156,7 +253,7 @@ jobs:
- name: Create checksums and manifest
working-directory: dist
run: |
version="${GITHUB_REF_NAME#v}"
version="$(node -p 'require("../npm/package.json").version')"
sha256sum opencode-pty-*.tar.gz > SHA256SUMS
artifacts='[]'
while read -r sha256 archive; do
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "opencode-pty"
version = "0.3.0"
version = "0.3.1"
edition = "2024"
rust-version = "1.90"
description = "Persistent PTY service for OpenCode"
Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,15 +343,18 @@ gh run view RUN_ID --repo anomalyco/opencode-pty --log-failed

Pushing a `vX.Y.Z` tag matching the version in `Cargo.toml` creates an unsigned
GitHub release. The release contains x86-64 and arm64 binaries for Linux GNU,
Linux musl, and macOS, plus `SHA256SUMS`, a machine-readable
Linux musl, macOS, and Windows (MSVC, statically linked C runtime), plus
`SHA256SUMS`, a machine-readable
`release-manifest.json`, and GitHub build-provenance attestations. Release
builds force Ghostty's Zig code generation to its baseline CPU target so
artifacts do not inherit instruction-set extensions from CI runners. Linux GNU
artifacts support glibc 2.30 and newer.
artifacts do not inherit instruction-set extensions from CI runners (Windows
builds set `OPENCODE_ZIG_CPU=baseline`, since the Unix Zig wrapper is a shell
script). Linux GNU artifacts support glibc 2.30 and newer.

Tagged releases also publish `@opencode-ai/pty` to npm with optional,
platform-specific binary packages. Installing the npm package selects the native
binary for the current platform and exposes its path as `binaryPath`:
binary for the current platform (`opencode-pty.exe` on Windows) and exposes
its path as `binaryPath`:

```js
import { binaryPath } from "@opencode-ai/pty"
Expand All @@ -362,8 +365,7 @@ git tag v0.1.0
push origin v0.1.0
```

Release artifacts do not yet include Windows builds. Platform signing will be
added later.
Platform signing will be added later.

## Current Limits

Expand Down
6 changes: 6 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ fn main() {
println!("cargo:rerun-if-changed=ghostty-revision");
println!("cargo:rerun-if-changed=src/ghostty/ffi.rs");
println!("cargo:rerun-if-env-changed=GHOSTTY_SOURCE_DIR");
println!("cargo:rerun-if-env-changed=OPENCODE_ZIG_CPU");

let revision = include_str!("ghostty-revision").trim();
assert_eq!(
Expand Down Expand Up @@ -96,6 +97,11 @@ fn main() {
} else {
"-Doptimize=ReleaseFast"
});
// Release builds pin a portable CPU. Unix releases use script/release-bin/zig;
// Windows cannot run that shell wrapper, so it sets this instead.
if let Ok(cpu) = env::var("OPENCODE_ZIG_CPU") {
build.arg(format!("-Dcpu={cpu}"));
}
build.arg("--prefix").arg(&install);
build.arg("--cache-dir").arg(out.join("zig-cache"));
if env::var("HOST").as_deref() != Ok(target.as_str()) {
Expand Down
8 changes: 6 additions & 2 deletions npm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ import { createRequire } from "node:module"
import path from "node:path"

const require = createRequire(import.meta.url)
const supported = process.platform === "darwin" || process.platform === "linux"
const supported = process.platform === "darwin" || process.platform === "linux" || process.platform === "win32"
const arch = process.arch === "arm64" || process.arch === "x64" ? process.arch : undefined
const libc = process.platform === "linux" ? (process.report.getReport().header.glibcVersionRuntime ? "gnu" : "musl") : ""
const suffix = [process.platform, arch, libc].filter(Boolean).join("-")

export const binaryPath =
supported && arch
? path.join(path.dirname(require.resolve(`@opencode-ai/pty-${suffix}/package.json`)), "bin", "opencode-pty")
? path.join(
path.dirname(require.resolve(`@opencode-ai/pty-${suffix}/package.json`)),
"bin",
process.platform === "win32" ? "opencode-pty.exe" : "opencode-pty",
)
: undefined
16 changes: 9 additions & 7 deletions npm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/pty",
"version": "0.3.0",
"version": "0.3.1",
"description": "Persistent PTY service for OpenCode",
"type": "module",
"license": "MIT",
Expand All @@ -24,12 +24,14 @@
"index.js"
],
"optionalDependencies": {
"@opencode-ai/pty-darwin-arm64": "0.3.0",
"@opencode-ai/pty-darwin-x64": "0.3.0",
"@opencode-ai/pty-linux-arm64-gnu": "0.3.0",
"@opencode-ai/pty-linux-arm64-musl": "0.3.0",
"@opencode-ai/pty-linux-x64-gnu": "0.3.0",
"@opencode-ai/pty-linux-x64-musl": "0.3.0"
"@opencode-ai/pty-darwin-arm64": "0.3.1",
"@opencode-ai/pty-darwin-x64": "0.3.1",
"@opencode-ai/pty-linux-arm64-gnu": "0.3.1",
"@opencode-ai/pty-linux-arm64-musl": "0.3.1",
"@opencode-ai/pty-linux-x64-gnu": "0.3.1",
"@opencode-ai/pty-linux-x64-musl": "0.3.1",
"@opencode-ai/pty-win32-arm64": "0.3.1",
"@opencode-ai/pty-win32-x64": "0.3.1"
},
"publishConfig": {
"access": "public"
Expand Down
9 changes: 6 additions & 3 deletions script/npm-packages.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const targets = [
{ target: "aarch64-unknown-linux-musl", platform: "linux", arch: "arm64", libc: "musl", suffix: "musl" },
{ target: "x86_64-unknown-linux-gnu", platform: "linux", arch: "x64", libc: "glibc", suffix: "gnu" },
{ target: "x86_64-unknown-linux-musl", platform: "linux", arch: "x64", libc: "musl", suffix: "musl" },
{ target: "aarch64-pc-windows-msvc", platform: "win32", arch: "arm64" },
{ target: "x86_64-pc-windows-msvc", platform: "win32", arch: "x64" },
]

await rm(output, { recursive: true, force: true })
Expand All @@ -27,12 +29,13 @@ for (const target of targets) {
const name = `@opencode-ai/pty-${suffix}`
const directory = path.join(output, `opencode-pty-${suffix}`)
const archive = path.join(dist, `opencode-pty-${manifest.version}-${target.target}.tar.gz`)
const binary = target.platform === "win32" ? "opencode-pty.exe" : "opencode-pty"
const temporary = await mkdtemp(path.join(os.tmpdir(), "opencode-pty-npm-"))
try {
execFileSync("tar", ["-xzf", archive, "-C", temporary])
const executable = path.join(temporary, `opencode-pty-${manifest.version}-${target.target}`, "opencode-pty")
const executable = path.join(temporary, `opencode-pty-${manifest.version}-${target.target}`, binary)
await mkdir(path.join(directory, "bin"), { recursive: true })
await copyFile(executable, path.join(directory, "bin", "opencode-pty"))
await copyFile(executable, path.join(directory, "bin", binary))
await copyFile(path.join(root, "LICENSE"), path.join(directory, "LICENSE"))
await writeFile(
path.join(directory, "package.json"),
Expand All @@ -46,7 +49,7 @@ for (const target of targets) {
os: [target.platform],
cpu: [target.arch],
...(target.libc ? { libc: [target.libc] } : {}),
exports: { "./package.json": "./package.json", "./bin/opencode-pty": "./bin/opencode-pty" },
exports: { "./package.json": "./package.json", [`./bin/${binary}`]: `./bin/${binary}` },
files: ["bin"],
publishConfig: { access: "public" },
},
Expand Down
Loading