Release #17
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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.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 | |
| - runner: windows-11-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 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 |