Skip to content

Binary releases in GitHub #477

Description

Goal

Ship a standalone hackagent executable per OS/arch attached to each GitHub Release, so a user can download one file and run hackagent with no Python/pip/uv install — the same experience uv, ruff, and gh itself provide. Today publish.yml already creates a GitHub Release on every v* tag (via actions/create-release@v1, right after uv build + uv publish to PyPI), but it carries no binary assets — the release page just links back to PyPI. This issue is about adding those assets to that same release.

Proposed approach: PyInstaller one-file build in a CI matrix

hackagent is a Click app ([project.scripts] hackagent = "hackagent.cli.main:cli" in pyproject.toml) that defaults to launching a Textual TUI (_launch_tui_default in hackagent/cli/bootstrap.py) when invoked with no subcommand. PyInstaller is the standard tool for this shape of app (pure-Python CLI + native-extension deps + TUI) and is what most comparable projects use for one-off no-python-required binaries.

Add a build-binaries job to .github/workflows/publish.yml, gated on the same v* tag push, running in parallel with the existing publish job:

build-binaries:
  strategy:
    matrix:
      include:
        - os: ubuntu-latest
          target: linux-x86_64
        - os: macos-latest
          target: macos-arm64
        - os: macos-13          # last Intel macos-* runner
          target: macos-x86_64
        - os: windows-latest
          target: windows-x86_64
  runs-on: ${{ matrix.os }}
  steps:
    - uses: actions/checkout@v7
    - uses: astral-sh/setup-uv@v7
      with: { version: "0.10.11" }
    - run: uv sync --extra pyinstaller   # see "new dependency" below
    - run: uv run pyinstaller packaging/hackagent.spec
    - run: <rename dist artifact to hackagent-${{ matrix.target }}[.exe]>
    - uses: actions/upload-release-asset@v1   # or softprops/action-gh-release, see note

The publish job's actions/create-release@v1 step is deprecated/unmaintained and only returns an upload URL awkwardly for matrix jobs; switching both jobs to softprops/action-gh-release@v2 (actively maintained, supports a files: glob and can be called once per matrix leg with append_body: true) is worth doing as part of this issue rather than fighting the old action across 4 parallel jobs.

Known packaging gotchas specific to this repo's dependency set

These need to be handled in the PyInstaller spec, not discovered at release time:

  • importlib.metadata.version("hackagent") is used both for --version and the version command (cli/main.py:72, hackagent/cli/main.py:260). A frozen PyInstaller binary has no installed distribution metadata by default — this raises PackageNotFoundError unless the spec adds --copy-metadata hackagent (or the code falls back to a version baked in at build time via an env var).
  • Textual ships its own CSS/asset files that PyInstaller's default import hook doesn't always collect — needs --collect-data textual. The app's own TUI code lives under hackagent/cli/tui/; worth double-checking during the build whether it loads any non-.py assets at runtime (fonts, icons) that also need --add-data.
  • faiss-cpu and other compiled-extension deps need the matrix build to run natively per target (no cross-compiling) — the matrix above already does this by using per-OS runners rather than QEMU/cross builds.
  • playwright (used by the WEB provider) requires a separate playwright install browser download regardless of packaging method — the binary cannot embed Chromium/Firefox. This should be called out explicitly wherever the binary is documented/installed (README/release notes), same caveat that exists for pip install hackagent today.
  • nicegui (local dashboard) serves its own static web assets from its package data — needs --collect-data nicegui, same class of issue as Textual.
  • One-file mode (--onefile) unpacks to a temp dir on every launch, which is slow for an app this size (~15 MB of first-party code before deps); --onedir (a folder + launcher) starts faster and is what most CLI-shaped PyInstaller distributions actually ship, at the cost of shipping a .zip/.tar.gz instead of a single file. Worth deciding onedir-in-an-archive vs true onefile before wiring the workflow, since it changes the release-asset naming and the install instructions.

New dev dependency

Add pyinstaller as an optional dependency group (e.g. [dependency-groups] packaging = ["pyinstaller>=6.0"] in pyproject.toml), not to the default dev group — it's a release-time tool the day-to-day contributor doesn't need.

Release asset naming

Match the convention CLI tools like uv/ripgrep use so an install script can predict the filename from uname:

hackagent-${VERSION}-linux-x86_64.tar.gz
hackagent-${VERSION}-macos-arm64.tar.gz
hackagent-${VERSION}-macos-x86_64.tar.gz
hackagent-${VERSION}-windows-x86_64.zip

Optional follow-up: install script

Once assets exist with predictable names, a curl -fsSL https://.../install.sh | bash one-liner (resolve OS/arch, download the matching release asset, extract to ~/.local/bin or similar) is a natural next step — but it's additive and shouldn't block getting the binaries themselves published. Track it as a follow-up rather than in this issue's acceptance criteria.

Acceptance criteria

  • packaging/hackagent.spec PyInstaller spec covering the metadata/Textual/nicegui data-collection gotchas above
  • pyinstaller added as its own dependency group, not dev
  • build-binaries matrix job in publish.yml (Linux x86_64, macOS arm64 + x86_64, Windows x86_64) producing the 4 archives above
  • Release job switched to an actively-maintained action that both publish and build-binaries can attach assets through (e.g. softprops/action-gh-release)
  • hackagent --version / hackagent version work correctly from the frozen binary (no PackageNotFoundError)
  • hackagent (no subcommand → TUI) and at least one subcommand (e.g. hackagent scan) smoke-tested from the built binary on each OS, not just import-tested
  • README/release notes call out the separate playwright install step needed for the WEB provider

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions