Skip to content

Release

Release #6

Workflow file for this run

name: Release
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*' # v0.2.0, v1.0.0, v0.2.0-rc.1, …
workflow_dispatch:
inputs:
tag:
description: 'Existing release tag to build Windows/CLI artifacts for (e.g. v1.0.0). The macOS DMG and Homebrew tap are NOT rebuilt on manual runs.'
required: true
default: 'v1.0.0'
# ── Platform note ─────────────────────────────────────────────────────────────
# macOS Apple Silicon is the primary GUI target. Lumen is a macOS-first product
# (system tray, ~/.claude/ integration, Tauri native APIs). A Windows GUI
# installer (NSIS .exe + MSI) is also produced for users who want a downloadable
# setup file; note the ~/.claude/ monitoring is macOS-oriented and may be only
# partially functional on Windows until a full port lands.
env:
PNPM_VERSION: '10'
NODE_VERSION: '22'
TRIPLE: 'aarch64-apple-darwin'
jobs:
# ════════════════════════════════════════════════════════════════════════════
# Job 0: Resolve the target tag/version for both push and manual dispatch
# ────────────────────────────────────────────────────────────────────────────
# On a tag push, GITHUB_REF_NAME is the tag. On workflow_dispatch it is the
# branch (e.g. "main"), so we read the tag from the input instead. Every other
# job consumes these outputs rather than GITHUB_REF_NAME directly.
# ════════════════════════════════════════════════════════════════════════════
resolve:
name: Resolve tag/version
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.r.outputs.tag }}
version: ${{ steps.r.outputs.version }}
steps:
- id: r
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
TAG="${{ inputs.tag }}"
else
TAG="${GITHUB_REF_NAME}"
fi
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
echo "Resolved tag=${TAG} version=${TAG#v}"
# ════════════════════════════════════════════════════════════════════════════
# Job 1: Build both artifacts on Apple Silicon (tag push only)
# ════════════════════════════════════════════════════════════════════════════
build:
name: Build (macOS Apple Silicon)
needs: resolve
if: github.event_name == 'push'
runs-on: macos-14 # M1 runner — aarch64 native
permissions:
contents: write # required to create a GitHub Release
outputs:
version: ${{ steps.meta.outputs.version }}
dmg_name: ${{ steps.checksums.outputs.dmg_name }}
tarball_name: ${{ steps.meta.outputs.tarball_name }}
dmg_sha256: ${{ steps.checksums.outputs.dmg_sha256 }}
tarball_sha256: ${{ steps.checksums.outputs.tarball_sha256 }}
steps:
# ── 1. Checkout ─────────────────────────────────────────────────────────
- uses: actions/checkout@v4
# ── 2. Version metadata ─────────────────────────────────────────────────
- name: Extract version from tag
id: meta
run: |
VERSION="${GITHUB_REF_NAME#v}" # strip leading 'v'
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "dmg_name=Lumen_${VERSION}_aarch64.dmg" >> "$GITHUB_OUTPUT"
echo "tarball_name=lumen-v${VERSION}-aarch64-apple-darwin.tar.gz" >> "$GITHUB_OUTPUT"
# ── 3. Node + pnpm ──────────────────────────────────────────────────────
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
# ── 4. Rust toolchain ───────────────────────────────────────────────────
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ env.TRIPLE }}
- uses: swatinem/rust-cache@v2
with:
workspaces: ". -> target"
cache-on-failure: true
# ── 5. Install frontend dependencies ────────────────────────────────────
- name: pnpm install
working-directory: lumenator
run: pnpm install --frozen-lockfile
# ── 6. Build sidecars (MUST run before tauri build) ─────────────────────
# lumen-daemon, lumen-mcp, lumen-tok, and the lumen CLI binary are built
# here and staged into lumenator/src-tauri/binaries/ with the target-triple
# suffix that Tauri's sidecar mechanism requires.
- name: Build sidecars
working-directory: lumenator
run: bash ./build-sidecar.sh
# ── 7. Build CLI tar.gz ─────────────────────────────────────────────────
# lumen-cli is also a sidecar (built above) but we ship it separately as
# a standalone tarball for the Homebrew formula.
- name: Package CLI tarball
id: cli
run: |
VERSION="${{ steps.meta.outputs.version }}"
TARBALL="${{ steps.meta.outputs.tarball_name }}"
mkdir -p dist
cp target/release/lumen dist/lumen
tar -czf "dist/${TARBALL}" -C dist lumen
rm dist/lumen
echo "tarball=dist/${TARBALL}" >> "$GITHUB_OUTPUT"
# ── 8. Build the Tauri app (.dmg) ───────────────────────────────────────
# NO SIGNING — unsigned build, users run xattr or install via cask.
#
# ┌─────────────────────────────────────────────────────────────────────┐
# │ SIGNING PLACEHOLDER — slot in notarization here when ready │
# │ │
# │ Required secrets (set in repo Settings → Secrets): │
# │ APPLE_CERTIFICATE base64-encoded .p12 Developer ID cert │
# │ APPLE_CERTIFICATE_PASSWORD passphrase for the .p12 │
# │ APPLE_ID AppleID email for notarytool │
# │ APPLE_TEAM_ID 10-char Team ID │
# │ APPLE_ID_PASSWORD app-specific password for notarytool │
# │ │
# │ Tauri signing env vars to add when implementing: │
# │ APPLE_SIGNING_IDENTITY, APPLE_ID, APPLE_PASSWORD, APPLE_TEAM_ID │
# │ See: https://tauri.app/distribute/sign/macos/ │
# └─────────────────────────────────────────────────────────────────────┘
- name: Build Tauri app
working-directory: lumenator
run: pnpm tauri build
env:
# Disable Tauri's code-sign attempt for unsigned builds
CI: true
# ── 9. Compute sha256 checksums ─────────────────────────────────────────
- name: Compute checksums
id: checksums
run: |
# Tauri names the DMG from tauri.conf.json version, not the git tag.
# Discover the actual file rather than constructing the path from the tag.
DMG=$(find target/release/bundle/dmg -name "Lumen_*_aarch64.dmg" | head -1)
[[ -z "$DMG" ]] && { echo "ERROR: DMG not found in target/release/bundle/dmg"; exit 1; }
DMG_FILENAME=$(basename "$DMG")
TARBALL="${{ steps.cli.outputs.tarball }}"
DMG_SHA=$(shasum -a 256 "$DMG" | awk '{print $1}')
TAR_SHA=$(shasum -a 256 "$TARBALL" | awk '{print $1}')
echo "dmg_sha256=${DMG_SHA}" >> "$GITHUB_OUTPUT"
echo "tarball_sha256=${TAR_SHA}" >> "$GITHUB_OUTPUT"
echo "dmg_path=${DMG}" >> "$GITHUB_OUTPUT"
echo "dmg_name=${DMG_FILENAME}" >> "$GITHUB_OUTPUT"
echo "DMG: $DMG"
echo "DMG sha256: $DMG_SHA"
echo "Tarball sha256: $TAR_SHA"
# ── 10. Create GitHub Release with both assets ──────────────────────────
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${GITHUB_REF_NAME}"
DMG_PATH="${{ steps.checksums.outputs.dmg_path }}"
TARBALL="${{ steps.cli.outputs.tarball }}"
DMG_NAME="${{ steps.checksums.outputs.dmg_name }}"
TARBALL_NAME="${{ steps.meta.outputs.tarball_name }}"
# Use CHANGELOG.md entry for this version as release notes, if present
NOTES_OPT=""
if [[ -f CHANGELOG.md ]]; then
# Extract just this version's block (between its header and the next one)
awk "/^## \[${VERSION}\]/{found=1; next} found && /^## \[/{exit} found" \
CHANGELOG.md > /tmp/release_notes.md
[[ -s /tmp/release_notes.md ]] && NOTES_OPT="--notes-file /tmp/release_notes.md"
fi
if [[ -z "$NOTES_OPT" ]]; then
if [[ -f RELEASE_NOTES.md ]]; then
NOTES_OPT="--notes-file RELEASE_NOTES.md"
else
NOTES_OPT="--generate-notes"
fi
fi
gh release create "$TAG" \
"${DMG_PATH}#${DMG_NAME}" \
"${TARBALL}#${TARBALL_NAME}" \
--title "Lumen ${TAG}" \
$NOTES_OPT
# ════════════════════════════════════════════════════════════════════════════
# Job 2: Cross-platform CLI artifacts (Linux + Windows)
# ────────────────────────────────────────────────────────────────────────────
# The GUI (.dmg) is macOS-only, but the `lumen` CLI is pure cross-platform Rust
# (SQLite compiled from source via rusqlite `bundled` — no system lib needed).
# These jobs build the CLI per target and attach it to the release the macOS
# build job created, so each platform gets a native `lumen` binary.
# ════════════════════════════════════════════════════════════════════════════
build-cli:
name: Build CLI (${{ matrix.name }})
# The GitHub Release must exist before we upload to it: on a tag push the
# macOS `build` job creates it; on a manual dispatch it already exists.
needs: [resolve, build]
if: always() && needs.resolve.result == 'success' && (github.event_name == 'workflow_dispatch' || needs.build.result == 'success')
permissions:
contents: write # required to upload assets to the release
strategy:
fail-fast: false
matrix:
include:
- name: linux-x86_64
os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- name: windows-x86_64
os: windows-latest
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.resolve.outputs.tag }}
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: swatinem/rust-cache@v2
with:
workspaces: ". -> target"
cache-on-failure: true
- name: Build CLI
run: cargo build -p lumen-cli --release --target ${{ matrix.target }}
# Package as .tar.gz on Unix; upload to the release with --clobber so
# re-runs replace rather than fail on an existing asset.
- name: Package + upload (Unix)
if: runner.os != 'Windows'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ needs.resolve.outputs.version }}"
TAG="${{ needs.resolve.outputs.tag }}"
ASSET="lumen-v${VERSION}-${{ matrix.target }}.tar.gz"
mkdir -p dist
cp "target/${{ matrix.target }}/release/lumen" dist/lumen
tar -czf "dist/${ASSET}" -C dist lumen
gh release upload "$TAG" "dist/${ASSET}" --clobber
# Package as .zip on Windows (lumen.exe).
- name: Package + upload (Windows)
if: runner.os == 'Windows'
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$version = "${{ needs.resolve.outputs.version }}"
$tag = "${{ needs.resolve.outputs.tag }}"
$asset = "lumen-v$version-${{ matrix.target }}.zip"
New-Item -ItemType Directory -Force -Path dist | Out-Null
Copy-Item "target/${{ matrix.target }}/release/lumen.exe" "dist/lumen.exe"
Compress-Archive -Path "dist/lumen.exe" -DestinationPath "dist/$asset" -Force
gh release upload "$tag" "dist/$asset" --clobber
# ════════════════════════════════════════════════════════════════════════════
# Job 3: Windows GUI installer (.exe setup + .msi)
# ────────────────────────────────────────────────────────────────────────────
# Builds the full Tauri app on a Windows runner and produces a downloadable
# NSIS setup .exe and an MSI, both attached to the release. Mirrors the macOS
# build's sidecar-staging step, but for the x86_64-pc-windows-msvc triple.
# ════════════════════════════════════════════════════════════════════════════
build-gui-windows:
name: Build GUI installer (Windows x86_64)
needs: [resolve, build]
if: always() && needs.resolve.result == 'success' && (github.event_name == 'workflow_dispatch' || needs.build.result == 'success')
runs-on: windows-latest
permissions:
contents: write # required to upload assets to the release
env:
TRIPLE_WIN: 'x86_64-pc-windows-msvc'
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.resolve.outputs.tag }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- uses: swatinem/rust-cache@v2
with:
workspaces: ". -> target"
cache-on-failure: true
- name: pnpm install
working-directory: lumenator
run: pnpm install --frozen-lockfile
# Stage the sidecars Tauri's externalBin expects, suffixed with the host
# target triple (.exe). Equivalent to lumenator/build-sidecar.sh on macOS.
# `cargo build -p lumen-mcp` emits BOTH lumen-mcp.exe and lumen-tok.exe.
- name: Build sidecars (Windows)
shell: pwsh
run: |
cargo build -p lumen-daemon -p lumen-mcp -p lumen-cli --release
$dest = "lumenator/src-tauri/binaries"
New-Item -ItemType Directory -Force -Path $dest | Out-Null
foreach ($bin in @("lumen-daemon","lumen-mcp","lumen-tok","lumen")) {
Copy-Item "target/release/$bin.exe" "$dest/$bin-$env:TRIPLE_WIN.exe" -Force
Write-Host "sidecar ready: binaries/$bin-$env:TRIPLE_WIN.exe"
}
# targets:"all" in tauri.conf.json → NSIS setup .exe + MSI. Tauri fetches
# the NSIS and WiX toolchains automatically on first build.
- name: Build Tauri app (NSIS + MSI)
working-directory: lumenator
run: pnpm tauri build
env:
CI: true
- name: Upload installers to release
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$tag = "${{ needs.resolve.outputs.tag }}"
$setup = Get-ChildItem -Path target/release/bundle/nsis -Filter *-setup.exe -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
$msi = Get-ChildItem -Path target/release/bundle/msi -Filter *.msi -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $setup) { Write-Error "NSIS setup .exe not found under target/release/bundle/nsis"; exit 1 }
Write-Host "Uploading $($setup.Name)"
gh release upload "$tag" "$($setup.FullName)" --clobber
if ($msi) {
Write-Host "Uploading $($msi.Name)"
gh release upload "$tag" "$($msi.FullName)" --clobber
} else {
Write-Host "MSI not produced (skipping)"
}
# ════════════════════════════════════════════════════════════════════════════
# Job 4: Update the Homebrew tap (tag push only, after macOS build succeeds)
# ════════════════════════════════════════════════════════════════════════════
update-tap:
name: Update Homebrew tap
needs: build
if: github.event_name == 'push'
runs-on: ubuntu-latest # no macOS toolchain needed for a git commit
steps:
- name: Checkout lumen (for formula templates)
uses: actions/checkout@v4
- name: Check TAP_PUSH_TOKEN availability
id: tap_token
env:
TAP_PUSH_TOKEN: ${{ secrets.TAP_PUSH_TOKEN }}
run: |
if [[ -n "$TAP_PUSH_TOKEN" ]]; then
echo "configured=true" >> "$GITHUB_OUTPUT"
else
echo "configured=false" >> "$GITHUB_OUTPUT"
echo "TAP_PUSH_TOKEN is not set; skipping Homebrew tap update."
fi
# TAP_PUSH_TOKEN is a GitHub Personal Access Token with `repo` scope on
# HackPoint/homebrew-tap. GITHUB_TOKEN is scoped to this repo and cannot
# push to another repo.
#
# To create the secret:
# 1. github.com → Settings → Developer settings → Personal access tokens (classic)
# 2. New token: name "tap-push", scope = repo (or fine-grained: Contents write
# on HackPoint/homebrew-tap)
# 3. Copy the token value
# 4. HackPoint/lumen → Settings → Secrets and variables → Actions → New secret
# Name: TAP_PUSH_TOKEN Value: (paste token)
- name: Checkout homebrew-tap
if: steps.tap_token.outputs.configured == 'true'
uses: actions/checkout@v4
with:
repository: HackPoint/homebrew-tap
token: ${{ secrets.TAP_PUSH_TOKEN }}
path: homebrew-tap
- name: Update formulae with new version + sha256
if: steps.tap_token.outputs.configured == 'true'
run: |
VERSION="${{ needs.build.outputs.version }}"
DMG_SHA="${{ needs.build.outputs.dmg_sha256 }}"
TAR_SHA="${{ needs.build.outputs.tarball_sha256 }}"
# ── Casks/lumen.rb ────────────────────────────────────────────────
CASK="homebrew-tap/Casks/lumen.rb"
sed -i "s/^ version \"[^\"]*\"/ version \"${VERSION}\"/" "$CASK"
sed -i "s/^ sha256 \"[^\"]*\"/ sha256 \"${DMG_SHA}\"/" "$CASK"
echo "Cask updated:"
grep -E 'version|sha256' "$CASK"
# ── Formula/lumen-cli.rb ──────────────────────────────────────────
FORMULA="homebrew-tap/Formula/lumen-cli.rb"
sed -i "s/^ version \"[^\"]*\"/ version \"${VERSION}\"/" "$FORMULA"
sed -i "s/^ sha256 \"[^\"]*\"/ sha256 \"${TAR_SHA}\"/" "$FORMULA"
echo "Formula updated:"
grep -E 'version|sha256' "$FORMULA"
- name: Commit + push tap update
if: steps.tap_token.outputs.configured == 'true'
working-directory: homebrew-tap
run: |
VERSION="${{ needs.build.outputs.version }}"
git config user.name "HackPoint"
git config user.email "6758579+HackPoint@users.noreply.github.com"
git add Casks/lumen.rb Formula/lumen-cli.rb
git diff --cached --quiet && echo "No tap changes needed." && exit 0
git commit -m "chore: bump lumen to v${VERSION}"
git push