Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 220 additions & 0 deletions .github/workflows/release-binaries.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# Per-platform PyInstaller build + GitHub Release upload.
#
# Triggered on v* tags (and manually via workflow_dispatch for fork
# validation). Builds a single-file ``wbox`` binary for each of the four
# supported targets, computes its SHA-256, and uploads the binaries plus
# a combined manifest as assets on the GitHub Release for the tag.
#
# The PyPI publish job lives in ci.yml and is intentionally not touched
# here — both workflows fire on ``v*`` tags but operate independently.
#
# ----------------------------------------------------------------------
# Manifest contract (LOCKED — downstream consumers: issue #42 install.sh
# rewrite, issue #43 install.ps1 parity)
# ----------------------------------------------------------------------
# Manifest filename: SHA256SUMS.txt
# Format: standard sha256sum-compatible. One record per line:
# <hex-sha256><two-spaces><filename>
# Sort order: ASCII-sorted by filename (stable across runs)
# Binary filenames:
# wbox-linux-x64
# wbox-macos-arm64
# wbox-macos-x64
# wbox-windows-x64.exe
# Manifest assets are attached to the same GitHub Release as the four
# binaries. Install scripts can fetch SHA256SUMS.txt and a single binary
# from the same release URL prefix and verify the binary against the
# matching line.
# ----------------------------------------------------------------------

name: Release Binaries

on:
push:
tags: ["v*"]
workflow_dispatch: {}

permissions:
contents: read

jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: linux-x64
binary: wbox-linux-x64
- os: macos-15-intel # Intel runner (x86_64) — macos-13 was retired Dec 2025
target: macos-x64
binary: wbox-macos-x64
- os: macos-14 # Apple Silicon runner (arm64)
target: macos-arm64
binary: wbox-macos-arm64
- os: windows-latest
target: windows-x64
binary: wbox-windows-x64.exe
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install runtime + build deps
run: |
python -m pip install --upgrade pip
pip install .
pip install pyinstaller

- name: Build single-file binary (Unix)
if: runner.os != 'Windows'
shell: bash
run: |
pyinstaller \
--onefile \
--name "${{ matrix.binary }}" \
--noconfirm \
--collect-data wealthbox_tools \
--copy-metadata wealthbox-cli \
scripts/wbox_entry.py
Comment on lines +79 to +85

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bundle package data into PyInstaller binaries

For the released binaries, commands such as wbox skills install and wbox skills upgrade load wealthbox_tools/skills/wealthbox-crm via importlib.resources, but this PyInstaller invocation only includes the Python entry script/modules. PyInstaller’s documented options require adding non-code assets explicitly (--add-data) or collecting package data (--collect-data, “Collect all data from the specified package or module”), so the --version smoke test can pass while the binary omits the skill template and later fails when users run those commands. Please add the bundled skill data to both PyInstaller invocations and smoke-test a resource-backed command.

Useful? React with 👍 / 👎.

Comment on lines +83 to +85

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Copy package metadata into the frozen binary

For these PyInstaller builds, the smoke test invokes --version, which calls importlib.metadata.version("wealthbox-cli") in src/wealthbox_tools/cli/main.py; PyInstaller’s docs state that distribution metadata is not collected by default and must be added with --copy-metadata/copy_metadata, while --collect-data wealthbox_tools only collects package data. As a result, every matrix binary can build successfully but then fail immediately at ./dist/${{ matrix.binary }} --version with PackageNotFoundError, preventing release assets from being published; add --copy-metadata wealthbox-cli to both PyInstaller invocations.

Useful? React with 👍 / 👎.

test -f "dist/${{ matrix.binary }}"
chmod +x "dist/${{ matrix.binary }}"

- name: Build single-file binary (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
pyinstaller `
--onefile `
--name "${{ matrix.binary }}" `
--noconfirm `
--collect-data wealthbox_tools `
--copy-metadata wealthbox-cli `
scripts/wbox_entry.py
if (-not (Test-Path "dist/${{ matrix.binary }}")) {
throw "expected dist/${{ matrix.binary }} to exist"
}

- name: Smoke-test binary
# `--version` only proves the entry point runs. `skills install`
# exercises bundled package data — it reads
# wealthbox_tools/skills/wealthbox-crm via importlib.resources — so
# this fails loudly if --collect-data wealthbox_tools didn't bundle
# the skill template. Runs against the fresh runner HOME with
# --no-bootstrap to skip the post-install confirm prompt.
run: |
./dist/${{ matrix.binary }} --version
./dist/${{ matrix.binary }} skills install --platform claude-code-user --no-bootstrap

- name: Compute SHA-256 (Unix)
if: runner.os != 'Windows'
shell: bash
working-directory: dist
run: |
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "${{ matrix.binary }}" > "${{ matrix.binary }}.sha256"
else
# macOS: shasum -a 256 emits sha256sum-compatible "<hash> <name>"
shasum -a 256 "${{ matrix.binary }}" > "${{ matrix.binary }}.sha256"
fi
cat "${{ matrix.binary }}.sha256"

- name: Compute SHA-256 (Windows)
if: runner.os == 'Windows'
shell: pwsh
working-directory: dist
run: |
$hash = (Get-FileHash -Algorithm SHA256 "${{ matrix.binary }}").Hash.ToLower()
# sha256sum format: "<hex> <filename>" (two spaces, LF line ending)
$line = "$hash ${{ matrix.binary }}`n"
[System.IO.File]::WriteAllText(
(Join-Path $PWD "${{ matrix.binary }}.sha256"),
$line
)
Get-Content "${{ matrix.binary }}.sha256"

- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: wbox-${{ matrix.target }}
path: |
dist/${{ matrix.binary }}
dist/${{ matrix.binary }}.sha256
if-no-files-found: error
retention-days: 7

manifest-and-release:
name: Combine manifest and upload to release
needs: [build]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download all build artifacts
uses: actions/download-artifact@v4
with:
path: artifacts

- name: Stage release files
shell: bash
run: |
mkdir -p release
# Each artifact directory contains one binary + one .sha256 file.
find artifacts -type f \( -name 'wbox-*' -a ! -name '*.sha256' \) -exec cp -v {} release/ \;
find artifacts -type f -name '*.sha256' -exec cp -v {} release/ \;
ls -la release

- name: Build combined SHA256SUMS.txt
shell: bash
working-directory: release
run: |
# Concatenate per-binary sums into a single sha256sum-compatible
# manifest, ASCII-sorted by filename for stability.
# Each .sha256 file is already in "<hex> <filename>" form.
cat *.sha256 | LC_ALL=C sort -k 2 > SHA256SUMS.txt
echo "----- SHA256SUMS.txt -----"
cat SHA256SUMS.txt
echo "--------------------------"
# Verify the manifest against the staged binaries as a sanity check.
sha256sum -c SHA256SUMS.txt

- name: Upload combined manifest artifact
uses: actions/upload-artifact@v4
with:
name: SHA256SUMS
path: release/SHA256SUMS.txt
if-no-files-found: error

- name: Upload assets to GitHub Release
if: startsWith(github.ref, 'refs/tags/')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
GH_REPO: ${{ github.repository }}
shell: bash
working-directory: release
run: |
# Ensure a release exists for the tag. If the PyPI publish job in
# ci.yml or a manual release-create created it first, --clobber on
# upload handles re-runs cleanly.
if ! gh release view "$TAG" >/dev/null 2>&1; then
gh release create "$TAG" \
--title "$TAG" \
--notes "Automated release for $TAG. Binaries built by release-binaries.yml; PyPI wheel published by ci.yml."
fi
# Upload all four binaries + the combined manifest. Per-platform
# .sha256 sidecars are intentionally not uploaded — SHA256SUMS.txt
# is the single source of truth for installers.
gh release upload "$TAG" \
wbox-linux-x64 \
wbox-macos-arm64 \
wbox-macos-x64 \
wbox-windows-x64.exe \
SHA256SUMS.txt \
--clobber
20 changes: 20 additions & 0 deletions scripts/wbox_entry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""PyInstaller entry shim for the wbox CLI.

PyInstaller's --onefile mode prefers a script file over a module:function
console-script reference. This shim simply imports the Typer app defined
at ``wealthbox_tools.cli.main:app`` and calls it, so the resulting binary
behaves identically to the ``wbox`` console script registered in
``pyproject.toml``.
"""

from __future__ import annotations

from wealthbox_tools.cli.main import app


def main() -> None:
app()


if __name__ == "__main__":
main()
Loading