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
1 change: 1 addition & 0 deletions .github/workflows/announce-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ permissions:
jobs:
announce:
name: Announce ${{ inputs.tag || github.event.release.tag_name }}
if: github.event_name == 'workflow_dispatch' || !contains(github.event.release.tag_name, '-subscription.')
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand Down
282 changes: 282 additions & 0 deletions .github/workflows/build-fork-macos.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
name: Build OpenScreen Subscription for macOS

on:
workflow_dispatch:
inputs:
whisper_run_id:
description: "Successful Build whisper-stt binaries run ID to package"
required: true
type: string

permissions:
contents: read

concurrency:
group: fork-macos-${{ github.ref_name }}
cancel-in-progress: false

jobs:
build:
name: macOS ${{ matrix.label }}
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
label: Apple Silicon
runner: macos-latest
binary_arch: arm64
- arch: x64
label: Intel
runner: macos-15-intel
binary_arch: x86_64
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout source
uses: actions/checkout@v7

- name: Setup Node.js
uses: ./.github/actions/setup

- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.11"

- name: Keep sharp on its prebuilt binary
run: npm rebuild sharp
env:
npm_config_build_from_source: "false"

- name: Stage reviewed speech runtime
run: bash scripts/stage-whisper-stt.sh darwin-${{ matrix.arch }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENSCREEN_WHISPER_RUN_ID: ${{ inputs.whisper_run_id }}

- name: Test ScreenCaptureKit package
run: npm run test:swift:mac

- name: Build ScreenCaptureKit helper
run: npm run build:native:mac
env:
OPENSCREEN_MAC_HELPER_ARCHS: ${{ matrix.arch }}

- name: Cache LGPL FFmpeg tree
uses: actions/cache@v6
with:
path: crates/thirdparty
key: fork-ffmpeg-macos-${{ matrix.arch }}-${{ hashFiles('scripts/fetch-ffmpeg-macos.mjs') }}

- name: Vendor pinned LGPL FFmpeg
run: npm run fetch:ffmpeg:mac

- name: Cache Rust dependencies
uses: actions/cache@v6
with:
path: |
~/.cargo/registry
~/.cargo/git
crates/target
key: fork-cargo-macos-${{ matrix.arch }}-${{ hashFiles('crates/Cargo.lock') }}
restore-keys: |
fork-cargo-macos-${{ matrix.arch }}-

- name: Build Metal compositor
run: npm run build:native:compositor:mac

- name: Build renderer and Electron main process
run: npm run build-vite

- name: Package fork app bundle
run: npx electron-builder --mac --${{ matrix.arch }} --dir --config electron-builder.fork.json --publish never
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false"

- name: Locate app bundle
id: app
run: |
set -euo pipefail
VERSION="$(node -p 'require("./package.json").version')"
APP="$(find "release/${VERSION}" -maxdepth 3 -name 'OpenScreen Subscription.app' -type d -print -quit)"
[ -n "$APP" ] || { echo "::error::OpenScreen Subscription.app was not packaged"; exit 1; }
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "path=${APP}" >> "$GITHUB_OUTPUT"

- name: Apply ad-hoc hardened-runtime signature
run: |
codesign --force --deep --sign - \
--options runtime \
--entitlements macos.entitlements \
"${{ steps.app.outputs.path }}"

- name: Verify bundle identity, architecture and payload
run: |
set -euo pipefail
APP="${{ steps.app.outputs.path }}"
BIN="${APP}/Contents/MacOS/OpenScreen Subscription"
RESOURCES="${APP}/Contents/Resources"
NATIVE="${RESOURCES}/electron/native/bin/darwin-${{ matrix.arch }}"

test -x "$BIN"
test -x "${NATIVE}/openscreen-screencapturekit-helper"
test -f "${NATIVE}/compositor_view.node"
test -x "${NATIVE}/whisper-stt-server"
test -f "${RESOURCES}/FORK.md"
test -f "${RESOURCES}/FORK-CHANGELOG.md"
test -f "${RESOURCES}/MACOS-INSTALLATION.md"
test -f "${RESOURCES}/LICENSE"
test -f "${RESOURCES}/THIRD-PARTY-NOTICES.md"

BUNDLE_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "${APP}/Contents/Info.plist")"
[ "$BUNDLE_ID" = "io.github.purpleprintai.openscreen-subscription" ] \
|| { echo "::error::Unexpected bundle ID ${BUNDLE_ID}"; exit 1; }

ACTUAL_ARCHS="$(lipo -archs "$BIN")"
case " ${ACTUAL_ARCHS} " in
*" ${{ matrix.binary_arch }} "*) ;;
*) echo "::error::Expected ${{ matrix.binary_arch }}, got ${ACTUAL_ARCHS}"; exit 1 ;;
esac

codesign --verify --deep --strict "$APP"
SIGNING_ID="$(codesign -dv --verbose=2 "$APP" 2>&1 | sed -n 's/^Identifier=//p')"
[ "$SIGNING_ID" = "$BUNDLE_ID" ] \
|| { echo "::error::Signature identifier ${SIGNING_ID} does not match ${BUNDLE_ID}"; exit 1; }

- name: Smoke-test packaged CLI boot
run: |
set -euo pipefail
BIN="${{ steps.app.outputs.path }}/Contents/MacOS/OpenScreen Subscription"
"$BIN" help > cli-help.txt
grep -q "OpenScreen CLI" cli-help.txt

- name: Create DMG
id: dmg
run: |
set -euo pipefail
VERSION="${{ steps.app.outputs.version }}"
case "${{ matrix.arch }}" in
arm64) LABEL="Apple-Silicon" ;;
x64) LABEL="Intel" ;;
esac
OUT="release/${VERSION}/OpenScreen-Subscription-${VERSION}-macOS-${LABEL}.dmg"
STAGING="release/${VERSION}/dmg-${{ matrix.arch }}"
rm -rf "$STAGING" "$OUT"
mkdir -p "$STAGING"
cp -R "${{ steps.app.outputs.path }}" "$STAGING/"
ln -s /Applications "$STAGING/Applications"
hdiutil create \
-srcfolder "$STAGING" \
-volname "OpenScreen Subscription" \
-fs HFS+ \
-format UDBZ \
"$OUT"
rm -rf "$STAGING"
echo "path=${OUT}" >> "$GITHUB_OUTPUT"

- name: Mount and verify DMG
run: |
set -euo pipefail
MOUNT="$(mktemp -d)"
cleanup() {
hdiutil detach "$MOUNT" -quiet 2>/dev/null || true
rmdir "$MOUNT" 2>/dev/null || true
}
trap cleanup EXIT
hdiutil attach -readonly -nobrowse -mountpoint "$MOUNT" "${{ steps.dmg.outputs.path }}" >/dev/null
APP="${MOUNT}/OpenScreen Subscription.app"
test -d "$APP"
test -L "${MOUNT}/Applications"
codesign --verify --deep --strict "$APP"
[ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "${APP}/Contents/Info.plist")" \
= "io.github.purpleprintai.openscreen-subscription" ]

- name: Write validation metadata
run: |
DMG="${{ steps.dmg.outputs.path }}" \
ARCH="${{ matrix.arch }}" \
VERSION="${{ steps.app.outputs.version }}" \
WHISPER_RUN_ID="${{ inputs.whisper_run_id }}" \
SOURCE_COMMIT="${{ github.sha }}" \
node --input-type=module <<'NODE'
import { createHash } from "node:crypto";
import { readFileSync, writeFileSync } from "node:fs";
const bytes = readFileSync(process.env.DMG);
const report = {
product: "OpenScreen Subscription",
version: process.env.VERSION,
architecture: process.env.ARCH,
bundleId: "io.github.purpleprintai.openscreen-subscription",
sourceCommit: process.env.SOURCE_COMMIT,
whisperBuildRunId: Number(process.env.WHISPER_RUN_ID),
signature: "ad-hoc hardened runtime",
notarized: false,
checks: {
nativePayloadPresent: true,
bundleIdentity: true,
architecture: true,
codeSignatureStructure: true,
packagedCliBoot: true,
dmgMount: true
},
dmg: {
name: process.env.DMG.split("/").at(-1),
bytes: bytes.length,
sha256: createHash("sha256").update(bytes).digest("hex")
}
};
writeFileSync(`macos-validation-${process.env.ARCH}.json`, `${JSON.stringify(report, null, 2)}\n`);
NODE
shasum -a 256 "${{ steps.dmg.outputs.path }}" > "SHA256-${{ matrix.arch }}.txt"

- name: Upload architecture package
uses: actions/upload-artifact@v7
with:
name: openscreen-subscription-macos-${{ matrix.arch }}
path: |
${{ steps.dmg.outputs.path }}
macos-validation-${{ matrix.arch }}.json
SHA256-${{ matrix.arch }}.txt
cli-help.txt
if-no-files-found: error
retention-days: 30

collect:
name: Collect macOS release assets
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout release documentation
uses: actions/checkout@v7

- name: Download Apple Silicon package
uses: actions/download-artifact@v8
with:
name: openscreen-subscription-macos-arm64
path: artifacts/arm64

- name: Download Intel package
uses: actions/download-artifact@v8
with:
name: openscreen-subscription-macos-x64
path: artifacts/x64

- name: Flatten and checksum release files
run: |
set -euo pipefail
mkdir release-assets
find artifacts -type f -name '*.dmg' -exec cp {} release-assets/ \;
find artifacts -type f -name 'macos-validation-*.json' -exec cp {} release-assets/ \;
cp MACOS-INSTALLATION.md release-assets/
(
cd release-assets
sha256sum *.dmg > SHA256SUMS-macOS.txt
)

- name: Upload combined release assets
uses: actions/upload-artifact@v7
with:
name: openscreen-subscription-macos-release
path: release-assets
if-no-files-found: error
retention-days: 30
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
tags:
- "v*"
- "!v*-subscription.*"
workflow_dispatch:
inputs:
arch:
Expand Down
33 changes: 33 additions & 0 deletions FORK-CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,39 @@ see [FORK.md](FORK.md).
| Context display | Separates estimated selected history, the user's reference value, and a verified context window for the active provider/model when available. |
| Distribution | Uses a separate app identity, user-data directory, installer name, update origin and release configuration from upstream. |

## 1.10.0-subscription.6 — 2026-08-31

### macOS validation prerelease

- Adds fork-branded DMG builds for both Apple Silicon and Intel Macs, preserving the
fork bundle identifier, product name, user-data separation and GitHub update origin.
- Verifies the ScreenCaptureKit helper, Metal compositor, speech runtime, executable
architecture, bundle identity, ad-hoc hardened-runtime signature, packaged CLI boot
and mounted DMG before collecting release assets.
- Pins speech-runtime staging to an explicit successful producer run. The previous
release workflow asked `gh run download` to inspect its own still-running job and
failed before either macOS package could be built.
- Prevents subscription tags and releases from entering the upstream-oriented build
and Discord announcement paths.

### Local AI compatibility

- Finds Codex CLI and Claude Code when OpenScreen is launched from Finder, whose PATH
normally omits Homebrew and user package-manager directories.
- Checks `~/.local/bin`, `~/.npm-global/bin`, `~/.volta/bin`, `~/Library/pnpm`,
`/opt/homebrew/bin` and `/usr/local/bin` without invoking a shell or copying CLI
credentials.

### Distribution status

- macOS packages remain prerelease-only until a real Mac completes the manual
record → edit → export checklist.
- The initial packages are ad-hoc signed and not notarized by Apple. Installation and
privacy-permission guidance is recorded in [MACOS-INSTALLATION.md](MACOS-INSTALLATION.md).
- Local validation passed 2,243 unit tests with 4 skipped across 189 files, both
application and test TypeScript checks, documentation, i18n and Biome. Biome
retained 14 pre-existing warnings.

## 1.10.0-subscription.5 — 2026-08-30

### Added and changed
Expand Down
24 changes: 24 additions & 0 deletions FORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,30 @@ The installer is unsigned unless an independently authorized signing identity is
configured. No upstream publisher identity is borrowed. Windows security prompts
must be handled by the user, not disabled by this fork or its build scripts.

## macOS prerelease procedure

macOS packages must be built natively on the matching GitHub-hosted architecture:
Apple Silicon on `macos-latest` and Intel on `macos-15-intel`. Cross-packaging an
Intel app from an Apple Silicon runner is not accepted because the capture helper,
FFmpeg and Metal compositor are host-architecture builds.

1. Run **Build whisper-stt binaries** against the release source and retain its
successful run ID.
2. Dispatch **Build OpenScreen Subscription for macOS** with that exact run ID.
This prevents a release/tag workflow from racing a still-running dependency build.
3. Package with `electron-builder.fork.json`; never inherit the upstream bundle ID,
product name or update repository.
4. Require both architecture jobs to verify the native payload, bundle ID, executable
architecture, code-signature structure, packaged CLI boot and mounted DMG.
5. Publish the combined DMGs, validation JSON and SHA-256 file only as a prerelease
until a real Mac completes the manual record → edit → export checklist.

Without independently authorized Apple Developer credentials, the workflow applies
an ad-hoc hardened-runtime signature so macOS privacy grants attach to the fork bundle
identifier. This is not Apple notarization. The release notes and
[MACOS-INSTALLATION.md](MACOS-INSTALLATION.md) must retain the Gatekeeper limitation;
never borrow upstream credentials or advise users to disable Gatekeeper globally.

## Attribution and redistribution

Keep `LICENSE` and `THIRD-PARTY-NOTICES.md` in source and installed resources.
Expand Down
Loading
Loading