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
203 changes: 203 additions & 0 deletions .github/workflows/desktop-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
name: desktop-smoke

on:
push:
branches: [dev]
pull_request:
branches: [dev]
workflow_dispatch:

concurrency:
group: desktop-smoke-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
changes:
runs-on: ubuntu-latest
outputs:
docs_only: ${{ steps.filter.outputs.docs_only }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- id: filter
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
HEAD_SHA: ${{ github.sha }}
run: |
set -euo pipefail

is_docs_path() {
case "$1" in
README.md|README_CN.md|packages/*/README.md|packages/opencode/specs/*|packages/opencode/BUN_SHELL_MIGRATION_PLAN.md|packages/app/create-effect-simplification-spec.md)
return 0
;;
*)
return 1
;;
esac
}

docs_only=false

if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
echo "docs_only=false" >> "$GITHUB_OUTPUT"
exit 0
fi

if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
BASE_SHA="$(git rev-list --max-parents=0 HEAD | tail -n 1)"
fi

mapfile -t changes < <(git diff --name-status --find-renames --find-copies "$BASE_SHA" "$HEAD_SHA" --)

if [ "${#changes[@]}" -gt 0 ]; then
docs_only=true

for change in "${changes[@]}"; do
IFS=$'\t' read -r status path1 path2 <<< "$change"

case "$status" in
A*|M*|T*|D*)
if ! is_docs_path "$path1"; then
docs_only=false
break
fi
;;
R*|C*)
if ! is_docs_path "$path1" || ! is_docs_path "$path2"; then
docs_only=false
break
fi
;;
*)
docs_only=false
break
;;
esac
done
fi

echo "docs_only=$docs_only" >> "$GITHUB_OUTPUT"

smoke-macos-arm64:
needs: changes
if: needs.changes.outputs.docs_only != 'true'
runs-on: macos-14
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "24"

- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.11"

- uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: |
bun-${{ runner.os }}-

- uses: actions/cache@v4
with:
path: |
~/Library/Caches/electron
~/Library/Caches/electron-builder
key: electron-${{ runner.os }}-${{ hashFiles('bun.lock', 'packages/desktop-electron/package.json') }}
restore-keys: |
electron-${{ runner.os }}-

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Build desktop app
run: bun run build
working-directory: packages/desktop-electron
env:
OPENCODE_CHANNEL: dev

- name: Package desktop app
run: npx electron-builder --mac dir --arm64 --publish never --config electron-builder.config.ts --config.mac.identity=- --config.mac.notarize=false
working-directory: packages/desktop-electron
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false"
OPENCODE_CHANNEL: dev

- name: Smoke check app bundle
run: |
set -euo pipefail

APP_PATH="dist/mac-arm64/PawWork Dev.app"
EXECUTABLE_PATH="$APP_PATH/Contents/MacOS/PawWork Dev"
INFO_PLIST_PATH="$APP_PATH/Contents/Info.plist"
ASAR_PATH="$APP_PATH/Contents/Resources/app.asar"
FRAMEWORK_PATH="$APP_PATH/Contents/Frameworks/Electron Framework.framework"
HELPER_APP_PATH="$APP_PATH/Contents/Frameworks/PawWork Dev Helper.app"

if [ ! -d "$APP_PATH" ]; then
echo "Expected app bundle at $APP_PATH"
exit 1
fi

if [ ! -x "$EXECUTABLE_PATH" ]; then
echo "Expected executable at $EXECUTABLE_PATH"
exit 1
fi

if [ ! -f "$INFO_PLIST_PATH" ]; then
echo "Expected Info.plist at $INFO_PLIST_PATH"
exit 1
fi

if [ ! -f "$ASAR_PATH" ]; then
echo "Expected app.asar at $ASAR_PATH"
exit 1
fi

if [ ! -d "$FRAMEWORK_PATH" ]; then
echo "Expected Electron Framework at $FRAMEWORK_PATH"
exit 1
fi

if [ ! -d "$HELPER_APP_PATH" ]; then
echo "Expected helper app at $HELPER_APP_PATH"
exit 1
fi

codesign -dv --verbose=2 "$APP_PATH" 2>&1 | tee /tmp/pawwork-codesign.txt
grep -q "Signature=adhoc" /tmp/pawwork-codesign.txt
working-directory: packages/desktop-electron

check:
if: always()
needs:
- changes
- smoke-macos-arm64
runs-on: ubuntu-latest
steps:
- name: Validate desktop smoke result
env:
DOCS_ONLY: ${{ needs.changes.outputs.docs_only }}
SMOKE_RESULT: ${{ needs.smoke-macos-arm64.result }}
run: |
set -euo pipefail

if [ "$DOCS_ONLY" = "true" ]; then
echo "Docs-only change, desktop smoke skipped."
exit 0
fi

if [ "$SMOKE_RESULT" != "success" ]; then
echo "smoke-macos-arm64=$SMOKE_RESULT"
exit 1
fi
115 changes: 115 additions & 0 deletions packages/opencode/test/github/desktop-smoke-workflow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { describe, expect, test } from "bun:test"
import { execFileSync } from "node:child_process"
import fs from "node:fs"
import path from "node:path"

const repoRoot = path.join(import.meta.dir, "../../../..")
const workflowPath = path.join(repoRoot, ".github", "workflows", "desktop-smoke.yml")

function readWorkflow() {
expect(fs.existsSync(workflowPath)).toBe(true)
return fs.readFileSync(workflowPath, "utf8")
}

type WorkflowStep = {
name?: string
run?: string
uses?: string
with?: Record<string, unknown>
env?: Record<string, string>
}

type WorkflowJob = {
if?: string
needs?: string | string[]
"runs-on"?: string
outputs?: Record<string, string>
steps?: WorkflowStep[]
}

type Workflow = {
name?: string
on?: Record<string, unknown>
permissions?: Record<string, string>
jobs?: Record<string, WorkflowJob>
}

function parseWorkflow() {
const parsed = execFileSync(
"ruby",
[
"-e",
`
require "json"
require "yaml"

data = YAML.load_file(ARGV[0])
data["on"] = data.delete(true) if data.key?(true)
puts JSON.generate(data)
`,
workflowPath,
],
{ encoding: "utf8" },
)

return JSON.parse(parsed) as Workflow
}

describe("desktop smoke workflow", () => {
test("defines a PR-safe macOS arm64 smoke build", () => {
const workflow = readWorkflow()
const parsed = parseWorkflow()
const jobs = parsed.jobs ?? {}
const changes = jobs.changes
const smoke = jobs["smoke-macos-arm64"]
const check = jobs.check
const smokeSteps = smoke?.steps ?? []
const packageStep = smokeSteps.find((step) => step.name === "Package desktop app")
const smokeStep = smokeSteps.find((step) => step.name === "Smoke check app bundle")

expect(parsed.name).toBe("desktop-smoke")
expect(parsed.on?.push).toEqual({ branches: ["dev"] })
expect(parsed.on?.pull_request).toEqual({ branches: ["dev"] })
expect(parsed.on?.workflow_dispatch).toEqual(null)
expect(parsed.permissions).toEqual({ contents: "read" })
expect(Object.keys(jobs).sort()).toEqual(["changes", "check", "smoke-macos-arm64"])

expect(changes?.outputs).toEqual({ docs_only: "${{ steps.filter.outputs.docs_only }}" })
expect(smoke?.needs).toBe("changes")
expect(smoke?.if).toBe("needs.changes.outputs.docs_only != 'true'")
expect(smoke?.["runs-on"]).toBe("macos-14")
expect(check?.if).toBe("always()")
expect(check?.needs).toEqual(["changes", "smoke-macos-arm64"])

expect(workflow).not.toContain("strategy:")
expect(workflow).not.toContain("matrix:")

expect(workflow).toContain("bun install --frozen-lockfile")
expect(workflow).toContain("bun run build")
expect(packageStep?.run).toContain(
"npx electron-builder --mac dir --arm64 --publish never --config electron-builder.config.ts",
)
expect(packageStep?.run).toContain("--config.mac.identity=-")
expect(packageStep?.run).toContain("--config.mac.notarize=false")
expect(packageStep?.env).toEqual({
CSC_IDENTITY_AUTO_DISCOVERY: "false",
OPENCODE_CHANNEL: "dev",
})

expect(smokeStep?.run).toContain("Expected app bundle at")
expect(smokeStep?.run).toContain("Expected executable at")
expect(smokeStep?.run).toContain("Expected Info.plist at")
expect(smokeStep?.run).toContain("Expected app.asar at")
expect(smokeStep?.run).toContain("Expected Electron Framework at")
expect(smokeStep?.run).toContain("Expected helper app at")
expect(smokeStep?.run).toContain("codesign -dv --verbose=2")
expect(smokeStep?.run).toContain('grep -q "Signature=adhoc"')

expect(workflow).toContain("smoke-macos-arm64.result")
expect(workflow).toContain("Docs-only change, desktop smoke skipped.")
expect(workflow).not.toContain("codesign --verify --deep --verbose=2")
expect(workflow).not.toContain("codesign --verify --deep --strict --verbose=2")
expect(workflow).not.toContain("pull_request_target")
expect(workflow).not.toContain("secrets.")
})
})
Loading