Skip to content

Release

Release #20

Workflow file for this run

name: Release
on:
workflow_dispatch:
inputs:
version:
description: "Specific version to release (e.g. 1.2.3, 1.2.3-beta.1, 1.2.3-rc.1), or leave empty for auto-detect from conventional commits"
required: false
type: string
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
version: ${{ steps.release_meta.outputs.version }}
prerelease: ${{ steps.release_meta.outputs.prerelease }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Determine next version (auto)
if: inputs.version == ''
id: semver
uses: ietf-tools/semver-action@v1
with:
token: ${{ github.token }}
noVersionBumpBehavior: patch
noNewCommitBehavior: patch
- name: Validate manual version
if: inputs.version != ''
run: |
if ! echo "${{ inputs.version }}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-(beta|rc)\.[0-9]+)?$'; then
echo "::error::Invalid version format '${{ inputs.version }}'. Expected X.Y.Z, X.Y.Z-beta.N, or X.Y.Z-rc.N"
exit 1
fi
- name: Set release metadata
id: release_meta
env:
VERSION: ${{ inputs.version != '' && inputs.version || steps.semver.outputs.nextStrict }}
run: |
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+-(beta|rc)\.[0-9]+$'; then
echo "prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "prerelease=false" >> "$GITHUB_OUTPUT"
fi
- name: Create tag
env:
VERSION: ${{ steps.release_meta.outputs.version }}
run: |
git tag "$VERSION"
git push origin "$VERSION"
- name: Generate release notes
id: changelog
uses: requarks/changelog-action@v1
with:
token: ${{ github.token }}
tag: ${{ steps.release_meta.outputs.version }}
writeToFile: false
useGitmojis: false
excludeTypes: ""
includeInvalidCommits: true
- name: Create release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.release_meta.outputs.version }}
body: ${{ steps.changelog.outputs.changes }}
prerelease: ${{ steps.release_meta.outputs.prerelease == 'true' }}
build-macos:
runs-on: macos-14
needs: release
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.release.outputs.version }}
- uses: oven-sh/setup-bun@v2
- uses: actions/cache@v5
with:
path: |
~/.bun/install/cache
~\.bun\install\cache
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: ${{ runner.os }}-bun-
- run: bun install --frozen-lockfile
- name: Set version in package.json
env:
VERSION: ${{ needs.release.outputs.version }}
run: |
bun -e "
const pkg = await Bun.file('package.json').json();
pkg.version = process.env.VERSION;
await Bun.write('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
- name: Build Electrobun app
run: bun run build -- --env=stable
- name: Import code signing certificate
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
CERT_FILE=$(mktemp /tmp/certificate.XXXXXX.p12)
KEYCHAIN_FILE=$(mktemp /tmp/keychain.XXXXXX)
rm -f "$KEYCHAIN_FILE"
KEYCHAIN_FILE="${KEYCHAIN_FILE}.keychain-db"
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_FILE"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_FILE"
security set-keychain-settings -lut 21600 "$KEYCHAIN_FILE"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_FILE"
security import "$CERT_FILE" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_FILE"
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_FILE"
security list-keychains -d user -s "$KEYCHAIN_FILE" login.keychain
echo "KEYCHAIN_FILE=$KEYCHAIN_FILE" >> "$GITHUB_ENV"
rm "$CERT_FILE"
- name: Sign and notarize macOS app
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
run: |
BUILD_DIR="build/stable-macos-arm64"
if [ ! -d "$BUILD_DIR" ]; then
echo "::error::Expected build output in $BUILD_DIR"
find build -maxdepth 2 -type d || true
exit 1
fi
APP_DIR=$(find "$BUILD_DIR" -maxdepth 1 -type d -name "*.app" | head -1)
if [ -z "$APP_DIR" ]; then
echo "::error::Could not find .app bundle in $BUILD_DIR"
exit 1
fi
IDENTITY="Developer ID Application: Cookielab s.r.o. ($APPLE_TEAM_ID)"
# Sign all nested binaries, frameworks, and dylibs from the inside out
find "$APP_DIR" -type f \( -name "*.dylib" -o -name "*.so" \) -exec \
codesign --force --options runtime --sign "$IDENTITY" --keychain "$KEYCHAIN_FILE" {} \;
find "$APP_DIR" -type d -name "*.framework" -exec \
codesign --force --options runtime --sign "$IDENTITY" --keychain "$KEYCHAIN_FILE" {} \;
# Sign any helper executables inside the bundle
find "$APP_DIR/Contents/MacOS" -type f -perm +111 -exec \
codesign --force --options runtime --sign "$IDENTITY" --keychain "$KEYCHAIN_FILE" {} \;
# Sign the app bundle itself
codesign --force --options runtime --sign "$IDENTITY" --keychain "$KEYCHAIN_FILE" "$APP_DIR"
echo "Verifying signature..."
codesign --verify --deep --strict "$APP_DIR"
# Create zip for notarization
NOTARIZE_ZIP=$(mktemp /tmp/notarize.XXXXXX.zip)
ditto -c -k --keepParent "$APP_DIR" "$NOTARIZE_ZIP"
echo "Submitting for notarization..."
xcrun notarytool submit "$NOTARIZE_ZIP" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_PASSWORD" \
--wait
rm "$NOTARIZE_ZIP"
echo "Stapling notarization ticket..."
xcrun stapler staple "$APP_DIR"
echo "APP_DIR=$APP_DIR" >> "$GITHUB_ENV"
- name: Package and upload macOS arm64 asset
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.release.outputs.version }}
run: |
ZIP_NAME="Klovi-${VERSION}-macos-arm64.zip"
ditto -c -k --keepParent "$APP_DIR" "$ZIP_NAME"
gh release upload "$VERSION" "$ZIP_NAME" --clobber
- name: Clean up keychain
if: always()
run: |
if [ -n "$KEYCHAIN_FILE" ] && [ -f "$KEYCHAIN_FILE" ]; then
security delete-keychain "$KEYCHAIN_FILE"
fi
build-linux:
runs-on: ${{ matrix.runner }}
needs: release
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
electrobun_arch: x64
artifact_arch: amd64
- runner: ubuntu-24.04-arm
electrobun_arch: arm64
artifact_arch: arm64
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.release.outputs.version }}
- uses: oven-sh/setup-bun@v2
- uses: actions/cache@v5
with:
path: |
~/.bun/install/cache
~\.bun\install\cache
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: ${{ runner.os }}-bun-
- run: bun install --frozen-lockfile
- name: Set version in package.json
env:
VERSION: ${{ needs.release.outputs.version }}
run: |
bun -e "
const pkg = await Bun.file('package.json').json();
pkg.version = process.env.VERSION;
await Bun.write('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
- name: Build Electrobun app
run: bun run build -- --env=stable
- name: Package and upload Linux asset
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.release.outputs.version }}
ELECTROBUN_ARCH: ${{ matrix.electrobun_arch }}
ARTIFACT_ARCH: ${{ matrix.artifact_arch }}
run: |
# Electrobun stable builds produce a self-extracting installer in artifacts/
INSTALLER="artifacts/stable-linux-${ELECTROBUN_ARCH}-Klovi-Setup.tar.gz"
if [ ! -f "$INSTALLER" ]; then
echo "::error::Expected installer at $INSTALLER"
find artifacts -type f 2>/dev/null || echo "No artifacts directory found"
find build -maxdepth 2 -type f -name "*.tar.gz" 2>/dev/null || true
exit 1
fi
ASSET_NAME="Klovi-${VERSION}-linux-${ARTIFACT_ARCH}.tar.gz"
cp "$INSTALLER" "$ASSET_NAME"
gh release upload "$VERSION" "$ASSET_NAME" --clobber
build-windows:
runs-on: ${{ matrix.runner }}
needs: release
strategy:
fail-fast: false
matrix:
include:
- runner: windows-2025
electrobun_arch: x64
artifact_arch: amd64
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.release.outputs.version }}
- uses: oven-sh/setup-bun@v2
- uses: actions/cache@v5
with:
path: |
~/.bun/install/cache
~\.bun\install\cache
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: ${{ runner.os }}-bun-
- run: bun install --frozen-lockfile
- name: Set version in package.json
env:
VERSION: ${{ needs.release.outputs.version }}
run: |
bun -e "
const pkg = await Bun.file('package.json').json();
pkg.version = process.env.VERSION;
await Bun.write('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
- name: Build Electrobun app
run: bun run build -- --env=stable
- name: Package and upload Windows asset
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.release.outputs.version }}
ELECTROBUN_ARCH: ${{ matrix.electrobun_arch }}
ARTIFACT_ARCH: ${{ matrix.artifact_arch }}
shell: pwsh
run: |
# Electrobun stable builds produce a self-extracting installer zip in artifacts/
$installer = "artifacts/stable-win-$env:ELECTROBUN_ARCH-Klovi-Setup.zip"
if (-not (Test-Path $installer)) {
Write-Host "::error::Expected installer at $installer"
if (Test-Path artifacts) {
Get-ChildItem artifacts -File -Recurse | ForEach-Object { $_.FullName }
}
if (Test-Path build) {
Get-ChildItem build -File -Recurse -Filter "*.zip" | ForEach-Object { $_.FullName }
}
exit 1
}
$assetName = "Klovi-$env:VERSION-windows-$env:ARTIFACT_ARCH.zip"
Copy-Item $installer $assetName
gh release upload $env:VERSION $assetName --clobber
update-cask:
if: ${{ needs.release.outputs.prerelease != 'true' }}
runs-on: ubuntu-latest
needs:
- release
- build-macos
environment: homebrew
steps:
- name: Download release asset and update Homebrew cask
env:
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
VERSION: ${{ needs.release.outputs.version }}
REPO: ${{ github.repository }}
run: |
set -e
ZIP_NAME="Klovi-${VERSION}-macos-arm64.zip"
curl -L -o "$ZIP_NAME" \
"https://github.com/${REPO}/releases/download/${VERSION}/${ZIP_NAME}"
SHA256=$(sha256sum "$ZIP_NAME" | cut -d' ' -f1)
BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}"
git clone "https://x-access-token:${GH_TOKEN}@github.com/cookielab/homebrew-tap.git"
cd homebrew-tap
# Remove old Formula if it exists
rm -f Formula/klovi.rb
mkdir -p Casks
cat > Casks/klovi.rb << CASK
cask "klovi" do
version "${VERSION}"
sha256 "${SHA256}"
url "${BASE_URL}/${ZIP_NAME}"
name "Klovi"
desc "Native desktop app for browsing and presenting AI coding session history"
homepage "https://github.com/${REPO}"
depends_on arch: :arm64
app "Klovi.app"
zap trash: [
"~/Library/Application Support/io.cookielab.klovi",
"~/Library/Preferences/io.cookielab.klovi.plist",
"~/Library/Caches/io.cookielab.klovi",
]
end
CASK
# Remove leading whitespace from heredoc indentation
sed -i 's/^ //' Casks/klovi.rb
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add -A
git commit -m "Update klovi cask to ${VERSION}"
git push