The release workflow reads the pinned @opencode-ai/sdk version with bash command-substitution syntax before caching the bundled OpenCode CLI artifact. On Windows jobs GitHub Actions defaults run steps to PowerShell, which treated VERSION= as a command and failed before the cache/build steps ran. Set shell: bash on the release workflow version-discovery steps so macOS and Windows use the same syntax. The smoke workflow already used bash for the Windows version step, which is why the smoke artifact could pass while the production Windows release job failed.
499 lines
18 KiB
YAML
499 lines
18 KiB
YAML
name: Release
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- 'v*'
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: 'Version to release (e.g., 0.1.0)'
|
|
required: true
|
|
type: string
|
|
dry_run:
|
|
description: 'Dry run (skip publishing)'
|
|
required: false
|
|
default: false
|
|
type: boolean
|
|
|
|
env:
|
|
CARGO_INCREMENTAL: 0
|
|
RUST_BACKTRACE: short
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
jobs:
|
|
create-release:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
release_id: ${{ steps.create_release.outputs.id }}
|
|
release_upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
version: ${{ steps.get_version.outputs.version }}
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
|
|
- name: Get version
|
|
id: get_version
|
|
run: |
|
|
if [[ -n "${{ github.event.inputs.version }}" ]]; then
|
|
echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
|
|
elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
|
echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "version=0.0.0-dev" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Extract changelog for release
|
|
env:
|
|
VERSION: ${{ steps.get_version.outputs.version }}
|
|
run: |
|
|
node - <<'NODE'
|
|
const fs = require('fs');
|
|
const version = process.env.VERSION;
|
|
const changelogPath = 'CHANGELOG.md';
|
|
if (!fs.existsSync(changelogPath)) {
|
|
throw new Error('CHANGELOG.md not found; add it before releasing.');
|
|
}
|
|
const changelog = fs.readFileSync(changelogPath, 'utf8');
|
|
const sections = changelog.split(/^## /m);
|
|
const section = sections.find(s => s.startsWith('[' + version + ']'));
|
|
if (!section) {
|
|
throw new Error('Changelog section [' + version + '] not found. Add a section like "## [' + version + '] - YYYY-MM-DD".');
|
|
}
|
|
const content = ('## ' + section).trim();
|
|
fs.mkdirSync('artifacts', { recursive: true });
|
|
fs.writeFileSync('artifacts/release-notes.md', content + '\n');
|
|
NODE
|
|
|
|
- name: Create GitHub Release
|
|
id: create_release
|
|
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
|
|
with:
|
|
tag_name: v${{ steps.get_version.outputs.version }}
|
|
draft: true
|
|
generate_release_notes: false
|
|
body_path: artifacts/release-notes.md
|
|
name: OpenChamber v${{ steps.get_version.outputs.version }}
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
publish-npm:
|
|
needs: create-release
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
|
|
- name: Setup bun
|
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: '20'
|
|
registry-url: 'https://registry.npmjs.org'
|
|
|
|
- name: Install dependencies
|
|
run: bun install --frozen-lockfile
|
|
|
|
- name: Build packages
|
|
run: bun run build
|
|
|
|
- name: Create npm tarball
|
|
working-directory: packages/web
|
|
run: npm pack
|
|
|
|
- name: Upload npm tarball to release
|
|
if: ${{ github.event.inputs.dry_run != 'true' }}
|
|
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
|
|
with:
|
|
tag_name: v${{ needs.create-release.outputs.version }}
|
|
files: packages/web/*.tgz
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Publish to npm
|
|
if: ${{ github.event.inputs.dry_run != 'true' }}
|
|
working-directory: packages/web
|
|
run: npm publish --access public
|
|
env:
|
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
|
|
build-desktop-electron-macos:
|
|
needs: create-release
|
|
runs-on: macos-26
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- target: aarch64-apple-darwin
|
|
arch: arm64
|
|
platform: darwin-aarch64
|
|
- target: x86_64-apple-darwin
|
|
arch: x64
|
|
platform: darwin-x86_64
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
|
|
- name: Setup bun
|
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: '20'
|
|
|
|
- name: Install dependencies
|
|
run: bun install --frozen-lockfile
|
|
|
|
- name: Get bundled OpenCode CLI version
|
|
id: opencode_cli_version
|
|
shell: bash
|
|
run: |
|
|
VERSION=$(node -p "require('./package.json').dependencies['@opencode-ai/sdk']")
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Cache bundled OpenCode CLI artifact
|
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
|
with:
|
|
path: packages/electron/.cache/opencode-cli
|
|
key: opencode-cli-${{ runner.os }}-${{ matrix.arch }}-${{ steps.opencode_cli_version.outputs.version }}
|
|
restore-keys: |
|
|
opencode-cli-${{ runner.os }}-${{ matrix.arch }}-
|
|
|
|
- name: Install Apple Certificate
|
|
env:
|
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
run: |
|
|
KEYCHAIN_PATH=$RUNNER_TEMP/electron-signing.keychain-db
|
|
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
|
|
|
|
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
|
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
|
|
|
echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12
|
|
security import $RUNNER_TEMP/certificate.p12 \
|
|
-P "$APPLE_CERTIFICATE_PASSWORD" \
|
|
-A -t cert -f pkcs12 \
|
|
-k "$KEYCHAIN_PATH"
|
|
|
|
security list-keychain -d user -s "$KEYCHAIN_PATH"
|
|
security set-key-partition-list -S apple-tool:,apple:,codesign: \
|
|
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
|
|
|
- name: Build Electron app
|
|
working-directory: packages/electron
|
|
env:
|
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
# rebuild-native.mjs reads this to target the right arch when
|
|
# cross-building (runner is arm64; x64 matrix needs the hint).
|
|
ELECTRON_BUILDER_ARCH: ${{ matrix.arch }}
|
|
run: |
|
|
bun run build:web-assets
|
|
bun run prepare:opencode-cli
|
|
bun run verify:opencode-cli
|
|
bun run bundle:main
|
|
# npmRebuild=false in package.json, so electron-builder won't
|
|
# recompile native deps on its own — we must rebuild against the
|
|
# target Electron ABI before packaging, otherwise better-sqlite3/
|
|
# node-pty/bun-pty crash on require inside the packaged app.
|
|
bun run rebuild:native
|
|
bunx electron-builder --mac --${{ matrix.arch }} --publish=never
|
|
bun run verify:opencode-cli:packaged
|
|
|
|
- name: Verify signature + entitlements + notarization
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
APP_DIR="packages/electron/dist/mac"
|
|
[ -d "packages/electron/dist/mac-arm64" ] && APP_DIR="packages/electron/dist/mac-arm64"
|
|
|
|
APP_PATH=$(find "$APP_DIR" -maxdepth 2 -name "*.app" -print -quit)
|
|
if [ -z "$APP_PATH" ]; then
|
|
echo "Error: .app not found under packages/electron/dist/mac*"
|
|
ls -la packages/electron/dist/
|
|
exit 1
|
|
fi
|
|
|
|
echo "Verifying $APP_PATH"
|
|
codesign -vv --deep --strict "$APP_PATH"
|
|
|
|
# Require hardened runtime
|
|
CS_INFO=$(codesign -dv --verbose=4 "$APP_PATH" 2>&1)
|
|
echo "$CS_INFO"
|
|
if ! echo "$CS_INFO" | grep -q "flags=.*runtime"; then
|
|
echo "Error: hardened runtime flag missing"
|
|
exit 1
|
|
fi
|
|
|
|
# Require notary ticket stapled
|
|
xcrun stapler validate "$APP_PATH"
|
|
|
|
ENTITLEMENTS=$(codesign -d --entitlements :- "$APP_PATH" 2>&1 || true)
|
|
if echo "$ENTITLEMENTS" | grep -q "com.apple.security.app-sandbox"; then
|
|
echo "Error: app sandbox entitlement is present"
|
|
exit 1
|
|
fi
|
|
for key in \
|
|
com.apple.security.cs.allow-jit \
|
|
com.apple.security.cs.allow-unsigned-executable-memory \
|
|
com.apple.security.cs.disable-library-validation
|
|
do
|
|
if ! echo "$ENTITLEMENTS" | grep -q "<key>$key</key>"; then
|
|
echo "Error: required entitlement missing: $key"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
- name: Upload DMG / ZIP / blockmaps to release
|
|
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
|
|
with:
|
|
tag_name: v${{ needs.create-release.outputs.version }}
|
|
files: |
|
|
packages/electron/dist/*.dmg
|
|
packages/electron/dist/*.zip
|
|
packages/electron/dist/*.blockmap
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Upload per-arch latest-mac.yml for merge
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: latest-yml-${{ matrix.target }}
|
|
path: packages/electron/dist/latest-mac.yml
|
|
retention-days: 1
|
|
|
|
build-desktop-electron-windows:
|
|
needs: create-release
|
|
# windows-latest currently resolves to a runner with Visual Studio 18,
|
|
# which this electron/node-gyp stack does not detect correctly.
|
|
runs-on: windows-2022
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- arch: x64
|
|
target: x86_64-pc-windows-msvc
|
|
platform: win32-x64
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
|
|
- name: Setup bun
|
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: '20'
|
|
|
|
- name: Install dependencies
|
|
run: bun install --frozen-lockfile
|
|
|
|
- name: Get bundled OpenCode CLI version
|
|
id: opencode_cli_version
|
|
shell: bash
|
|
run: |
|
|
VERSION=$(node -p "require('./package.json').dependencies['@opencode-ai/sdk']")
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Cache bundled OpenCode CLI artifact
|
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
|
with:
|
|
path: packages/electron/.cache/opencode-cli
|
|
key: opencode-cli-${{ runner.os }}-${{ matrix.arch }}-${{ steps.opencode_cli_version.outputs.version }}
|
|
restore-keys: |
|
|
opencode-cli-${{ runner.os }}-${{ matrix.arch }}-
|
|
|
|
- name: Build web assets
|
|
working-directory: packages/electron
|
|
run: bun run build:web-assets
|
|
|
|
- name: Prepare bundled OpenCode CLI
|
|
working-directory: packages/electron
|
|
shell: bash
|
|
run: |
|
|
bun run prepare:opencode-cli
|
|
bun run verify:opencode-cli
|
|
|
|
- name: Bundle main process
|
|
working-directory: packages/electron
|
|
run: bun run bundle:main
|
|
|
|
- name: Rebuild native modules
|
|
working-directory: packages/electron
|
|
shell: bash
|
|
# npmRebuild=false in package.json, so electron-builder won't
|
|
# recompile native deps on its own — we must rebuild against the
|
|
# target Electron ABI before packaging.
|
|
run: node ./scripts/rebuild-native.mjs
|
|
|
|
- name: Build Windows app
|
|
working-directory: packages/electron
|
|
shell: bash
|
|
run: |
|
|
node ./scripts/package.mjs --win --${{ matrix.arch }} --publish=never
|
|
bun run verify:opencode-cli:packaged
|
|
|
|
- name: Upload installer to release
|
|
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
|
|
with:
|
|
tag_name: v${{ needs.create-release.outputs.version }}
|
|
files: |
|
|
packages/electron/dist/*.exe
|
|
packages/electron/dist/*.blockmap
|
|
packages/electron/dist/latest.yml
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Upload update manifest as artifact
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: latest-yml-${{ matrix.target }}
|
|
path: packages/electron/dist/latest.yml
|
|
retention-days: 1
|
|
|
|
combine-electron-manifests:
|
|
needs: [create-release, build-desktop-electron-macos]
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: '20'
|
|
|
|
- name: Download per-arch latest-mac.yml
|
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
|
with:
|
|
pattern: latest-yml-*-apple-darwin
|
|
path: artifacts
|
|
|
|
- name: Finalize combined latest-mac.yml
|
|
env:
|
|
LATEST_YML_DIR: ${{ github.workspace }}/artifacts
|
|
GH_REPO: ${{ github.repository }}
|
|
OPENCHAMBER_VERSION: ${{ needs.create-release.outputs.version }}
|
|
run: node packages/electron/scripts/finalize-latest-yml.mjs
|
|
|
|
- name: Upload combined manifests to release
|
|
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
|
|
with:
|
|
tag_name: v${{ needs.create-release.outputs.version }}
|
|
files: |
|
|
${{ runner.temp }}/latest-mac.yml
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
finalize-release:
|
|
needs: [create-release, build-desktop-electron-macos, build-desktop-electron-windows, publish-npm, combine-electron-manifests]
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
|
DISCORD_UPDATE_ROLE_ID: ${{ secrets.DISCORD_UPDATE_ROLE_ID }}
|
|
steps:
|
|
- name: Publish release
|
|
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
|
|
with:
|
|
tag_name: v${{ needs.create-release.outputs.version }}
|
|
draft: false
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Send release to Discord
|
|
if: ${{ env.DISCORD_WEBHOOK_URL != '' }}
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
VERSION: ${{ needs.create-release.outputs.version }}
|
|
REPOSITORY: ${{ github.repository }}
|
|
UPDATE_ROLE_ID: ${{ env.DISCORD_UPDATE_ROLE_ID }}
|
|
run: |
|
|
node - <<'NODE'
|
|
(async () => {
|
|
const tag = `v${process.env.VERSION}`;
|
|
const repo = process.env.REPOSITORY;
|
|
const rawRoleId = (process.env.UPDATE_ROLE_ID || '').trim();
|
|
const updateRoleId = /^\d+$/.test(rawRoleId) ? rawRoleId : '';
|
|
|
|
const releaseRes = await fetch(`https://api.github.com/repos/${repo}/releases/tags/${tag}`, {
|
|
headers: {
|
|
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
|
Accept: 'application/vnd.github+json',
|
|
},
|
|
});
|
|
|
|
if (!releaseRes.ok) {
|
|
const body = await releaseRes.text();
|
|
throw new Error(`Failed to fetch release ${tag}: ${releaseRes.status} ${body}`);
|
|
}
|
|
|
|
const release = await releaseRes.json();
|
|
const description = (release.body || `OpenChamber ${tag} released.`).slice(0, 4096);
|
|
const mention = updateRoleId ? `<@&${updateRoleId}>` : '';
|
|
|
|
const payload = {
|
|
username: 'OpenChamber Releases',
|
|
...(mention ? { content: mention } : {}),
|
|
...(updateRoleId
|
|
? {
|
|
allowed_mentions: {
|
|
roles: [updateRoleId],
|
|
},
|
|
}
|
|
: {}),
|
|
embeds: [
|
|
{
|
|
title: release.name || `OpenChamber ${tag}`,
|
|
url: release.html_url,
|
|
description,
|
|
color: 2105893,
|
|
footer: { text: 'OpenChamber Changelog' },
|
|
},
|
|
],
|
|
};
|
|
|
|
const discordRes = await fetch(process.env.DISCORD_WEBHOOK_URL, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
if (!discordRes.ok) {
|
|
const body = await discordRes.text();
|
|
throw new Error(`Failed to send Discord release: ${discordRes.status} ${body}`);
|
|
}
|
|
})().catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|
|
NODE
|
|
|
|
- name: Trigger openchamber-website site refresh (optional)
|
|
env:
|
|
WEBSITE_REPO: openchamber/openchamber-website
|
|
WEBSITE_TOKEN: ${{ secrets.OPENCHAMBER_WEBSITE_REPO_TOKEN }}
|
|
VERSION: ${{ needs.create-release.outputs.version }}
|
|
run: |
|
|
if [ -z "$WEBSITE_TOKEN" ]; then
|
|
echo "OPENCHAMBER_WEBSITE_REPO_TOKEN not set; skip site refresh dispatch."
|
|
exit 0
|
|
fi
|
|
|
|
curl --fail-with-body -sS -X POST \
|
|
-H "Authorization: Bearer $WEBSITE_TOKEN" \
|
|
-H "Accept: application/vnd.github+json" \
|
|
https://api.github.com/repos/$WEBSITE_REPO/dispatches \
|
|
-d @- <<JSON
|
|
{
|
|
"event_type": "site_refresh_requested",
|
|
"client_payload": {
|
|
"source_repo": "${{ github.repository }}",
|
|
"release_tag": "v$VERSION"
|
|
}
|
|
}
|
|
JSON
|