Release #6
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: | |
| 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 |