From 79def36bc32216c95ee76b79e1a7c2e701da5cff Mon Sep 17 00:00:00 2001 From: Kadin Bullock Date: Fri, 8 May 2026 08:40:33 -0600 Subject: [PATCH 1/5] ci: per-platform PyInstaller build + release workflow (#33) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds .github/workflows/release-binaries.yml — a four-target matrix (linux-x64, macos-x64, macos-arm64, windows-x64) that runs PyInstaller on v* tags and attaches the binaries plus a SHA-256 manifest to the GitHub Release. Manifest contract (locked for downstream consumers #42, #43): - Filename: SHA256SUMS.txt - Format: standard sha256sum-compatible - Binary names: wbox-[.exe] The existing publish (PyPI) job in ci.yml is unchanged. Closes #33 Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release-binaries.yml | 215 +++++++++++++++++++++++++ scripts/wbox_entry.py | 20 +++ 2 files changed, 235 insertions(+) create mode 100644 .github/workflows/release-binaries.yml create mode 100644 scripts/wbox_entry.py diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml new file mode 100644 index 0000000..6fefdcb --- /dev/null +++ b/.github/workflows/release-binaries.yml @@ -0,0 +1,215 @@ +# 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: +# +# 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 + shell: bash + - os: macos-13 # Intel runner (x86_64) + target: macos-x64 + binary: wbox-macos-x64 + shell: bash + - os: macos-14 # Apple Silicon runner (arm64) + target: macos-arm64 + binary: wbox-macos-arm64 + shell: bash + - os: windows-latest + target: windows-x64 + binary: wbox-windows-x64.exe + shell: pwsh + 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 + shell: ${{ matrix.shell }} + 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 \ + scripts/wbox_entry.py + 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 ` + scripts/wbox_entry.py + if (-not (Test-Path "dist/${{ matrix.binary }}")) { + throw "expected dist/${{ matrix.binary }} to exist" + } + + - name: Smoke-test binary + shell: ${{ matrix.shell }} + run: | + ./dist/${{ matrix.binary }} --version + + - 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 " " + 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: " " (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 " " 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 diff --git a/scripts/wbox_entry.py b/scripts/wbox_entry.py new file mode 100644 index 0000000..db31318 --- /dev/null +++ b/scripts/wbox_entry.py @@ -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() From 03606819b3136746c03a241d52cf83c9447cf79d Mon Sep 17 00:00:00 2001 From: Kadin Bullock Date: Fri, 8 May 2026 09:21:00 -0600 Subject: [PATCH 2/5] fix: bundle wealthbox_tools package data in PyInstaller binaries (PR #63) Codex auto-review P2: --version smoke test passes even if the binary omits skills/wealthbox-crm, so users only discover the missing template when running `wbox skills install` or `wbox skills upgrade`. Both load wealthbox_tools/skills/wealthbox-crm via importlib.resources, which PyInstaller does not bundle by default for non-Python assets. Add --collect-data wealthbox_tools to both PyInstaller invocations (Unix and Windows) so the entire skills/ tree is captured. Extend the smoke test to invoke `skills install --platform claude-code-user --no-bootstrap`, which exercises the bundled resource and fails the build if --collect-data didn't pull the template in. --- .github/workflows/release-binaries.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 6fefdcb..8e33c3a 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -85,6 +85,7 @@ jobs: --onefile \ --name "${{ matrix.binary }}" \ --noconfirm \ + --collect-data wealthbox_tools \ scripts/wbox_entry.py test -f "dist/${{ matrix.binary }}" chmod +x "dist/${{ matrix.binary }}" @@ -97,15 +98,23 @@ jobs: --onefile ` --name "${{ matrix.binary }}" ` --noconfirm ` + --collect-data wealthbox_tools ` 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. shell: ${{ matrix.shell }} 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' From cd8d0c9fafb0ca3c16d839aef778f2894d598448 Mon Sep 17 00:00:00 2001 From: Kadin Bullock Date: Fri, 8 May 2026 09:39:49 -0600 Subject: [PATCH 3/5] fix: switch x64 macOS runner to macos-15-intel (PR #63) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub Actions retired the macos-13 label in December 2025 and recommends macos-15-intel for x86_64 workloads going forward. With macos-13 the x64 matrix leg would never schedule on a real v* tag, failing the required build job and blocking manifest-and-release from publishing any binaries. Critical Codex P1 — fixed in PR rather than deferred to an issue because this PR's entire purpose is producing release binaries; without a working x64 macOS leg the workflow has zero release value. --- .github/workflows/release-binaries.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 8e33c3a..54f642e 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -49,7 +49,7 @@ jobs: target: linux-x64 binary: wbox-linux-x64 shell: bash - - os: macos-13 # Intel runner (x86_64) + - os: macos-15-intel # Intel runner (x86_64) — macos-13 was retired Dec 2025 target: macos-x64 binary: wbox-macos-x64 shell: bash From 9814f2f79ece946f1202525983b8671221595b34 Mon Sep 17 00:00:00 2001 From: Kadin Bullock Date: Fri, 8 May 2026 11:37:19 -0600 Subject: [PATCH 4/5] fix: copy wealthbox-cli distribution metadata into PyInstaller binaries (PR #63) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PyInstaller's --collect-data only collects package data files; it does NOT copy distribution metadata. Five modules call importlib.metadata.version ("wealthbox-cli") (cli/main.py, cli/doctor.py, cli/skills.py, cli/_skill_bootstrap.py, firm/archive.py), so without --copy-metadata the smoke-test step's `--version` invocation would raise PackageNotFoundError on every matrix job, blocking manifest-and-release from publishing. Codex P1 (3rd pass on PR #63). Like the macos-13 fix, this is the exception case: workflow's whole purpose is producing release binaries, so a dead smoke test means zero release value. After this, the next signal worth acting on is workflow_dispatch on a fork — Codex's static analysis has hit its limit; only running the workflow exercises the full PyInstaller bundling chain. --- .github/workflows/release-binaries.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 54f642e..982c4d6 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -86,6 +86,7 @@ jobs: --name "${{ matrix.binary }}" \ --noconfirm \ --collect-data wealthbox_tools \ + --copy-metadata wealthbox-cli \ scripts/wbox_entry.py test -f "dist/${{ matrix.binary }}" chmod +x "dist/${{ matrix.binary }}" @@ -99,6 +100,7 @@ jobs: --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" From edd39c64155c90d38161c75261cdfe52f55b1273 Mon Sep 17 00:00:00 2001 From: Kadin Bullock Date: Fri, 8 May 2026 11:46:40 -0600 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20drop=20matrix.shell=20=E2=80=94=20wo?= =?UTF-8?q?rkflow's=20shell=20field=20can't=20reference=20matrix=20context?= =?UTF-8?q?=20(PR=20#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub Actions evaluates a step's `shell:` field before matrix expansion, so `shell: ${{ matrix.shell }}` produces a workflow-syntax parse error ("Unrecognized named-value: 'matrix'") that blocks every trigger, including workflow_dispatch. Three earlier push-events failed silently for this reason. Removing the matrix.shell references and the now-unused `shell:` keys on matrix entries. The build steps that explicitly need pwsh (Windows backtick line continuations) keep their hardcoded `shell: pwsh`; the install/smoke-test steps fall back to GitHub's per-OS default (bash on Linux/macOS, pwsh on Windows), which is exactly what matrix.shell was specifying anyway. --- .github/workflows/release-binaries.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 982c4d6..4a0a50e 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -48,19 +48,15 @@ jobs: - os: ubuntu-latest target: linux-x64 binary: wbox-linux-x64 - shell: bash - os: macos-15-intel # Intel runner (x86_64) — macos-13 was retired Dec 2025 target: macos-x64 binary: wbox-macos-x64 - shell: bash - os: macos-14 # Apple Silicon runner (arm64) target: macos-arm64 binary: wbox-macos-arm64 - shell: bash - os: windows-latest target: windows-x64 binary: wbox-windows-x64.exe - shell: pwsh steps: - name: Checkout uses: actions/checkout@v4 @@ -71,7 +67,6 @@ jobs: python-version: "3.12" - name: Install runtime + build deps - shell: ${{ matrix.shell }} run: | python -m pip install --upgrade pip pip install . @@ -113,7 +108,6 @@ jobs: # 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. - shell: ${{ matrix.shell }} run: | ./dist/${{ matrix.binary }} --version ./dist/${{ matrix.binary }} skills install --platform claude-code-user --no-bootstrap