Skip to content

feat(installer): tag-triggered Windows installer, plus a dry run that publishes nothing - #2

Merged
joepetjr merged 2 commits into
mainfrom
feat/windows-installer
Aug 3, 2026
Merged

joepetjr merged 2 commits into
mainfrom
feat/windows-installer

Conversation

@joepetjr

@joepetjr joepetjr commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports the reviewed installer pattern from keycut (PR #2/#3). Pushing a v* tag builds a Windows
installer on windows-latest — PyInstaller one-file exe, wrapped by Inno Setup, installed to
Program Files, appended to the machine PATH, registered in Add/Remove Programs — and attaches it to
that tag's release. workflow_dispatch runs the identical build and stops short of publishing,
leaving the installer as an artifact, so the Windows half can be proven without a public release.

The external requirement, established from source

Claude Code (claude) must be installed separately and be on PATH. This is not incidental:
driver.py:85-96 builds ["claude", "-p", prompt, "--agent", …] and driver.py:100 runs it via
subprocess.run. Every unit of work is one such process, so with no claude binary every unit
returns FLAGGED. Corroborated at pyproject.toml:23-24 and README.md:44-50. The installer's
finish page and the README both say so, with the install command.

Authentication is inherited and never handled here — driver.py:9-10 states this module "never
reads, sets, or forwards credentials of its own", and a grep for API_KEY|api_key|getenv|requests| urllib|http across the package returns no credential handling. The README says only: run claude
once and sign in. No mechanism is invented, and nothing about pricing, quota or plan entitlements
appears anywhere.

The install command is documentation-verified, not recalled: winget install Anthropic.ClaudeCode
on the finish page, with the native installer and claude --version in the README, taken from
code.claude.com/docs/en/setup.

The __main__ hazard, resolved by execution

The entry point is claude_batch_runner.__main__:main, and that module carries its own
if __name__ == "__main__": sys.exit(main()) at lines 60-61 — a launcher that re-triggered it would
run argparse twice. Proved by running it that importing as a submodule binds __name__ to
claude_batch_runner.__main__, so the guard does not fire:

import OK; module __name__ = claude_batch_runner.__main__
PROBE PASSED: no premature exit

Had it fired, argparse would have exited 2 on the missing --spec. The launcher docstring records why.

Test plan

  • python -m PyInstaller --onefile --console --name claude-batch-runner … → build complete
  • dist/claude-batch-runner --help → exit 0; --spec examples/demo-campaign.json --dry-run →
    exit 0 with a full valid plan, so real logic is reached inside the freeze
  • Frozen size 8.1 MB (Linux ELF; the Windows PE will differ)
  • PyInstaller warn file: 13 missing-module lines, all Windows-only stdlib, macOS-only, or
    PyInstaller-internal. Zero package modules missing → no --hidden-import
  • pytest -q → 65 passed, re-run unpiped by the dispatcher
  • .iss pure ASCII, no BOM, [Registry] PATH on one line with zero \ continuations
  • Workflow YAML parses; two mutually-exclusive guards, no build step guarded

Lint is pre-existing, not a regression: baseline at 625a44b is identically Found 9 errors /
3 files would be reformatted, all in files this PR does not touch. The launcher alone is clean.
ci.yml runs pytest only, so no lint job gates this.

Unverified until a real Windows run

The entire Windows half: the ISCC compile, the HKLM PATH add/remove Pascal (needs a real
install/uninstall, not just a build), the Windows freeze, elevation/SmartScreen, and the
gh release create-vs-upload branch. Merge this after keycut's dry run has gone green — the
same chain is now in four repos and a flaw found once should be fixed once.

Minor and pre-existing: --help prints usage: claude_batch_runner, not the exe name, from prog=
at __main__.py:18. Fixing it means touching src/, which is out of scope here.

Diff 285 sum (cap 300).

…t Claude Code

Pushing a v* tag now builds a PyInstaller one-file exe on windows-latest,
wraps it in an Inno Setup installer, and attaches it to that tag's GitHub
release. The installer lands in Program Files, appends itself to the machine
PATH (HKLM), and registers an uninstaller that removes exactly that PATH entry.

The console entry point is a module function targeting `__main__`, so
PyInstaller is aimed at installer/claude_batch_runner_launcher.py. Importing
claude_batch_runner.__main__ binds __name__ to that dotted path rather than
"__main__", so the module's own guard does not fire and argparse does not run
twice; only the launcher is __main__ in the frozen exe.

Two things the build refuses to paper over. The workflow fails a tag whose
version disagrees with pyproject.toml, and the .iss declares no default
AppVersion, so ISCC errors rather than emitting a 0.0.0-dev installer. Inno
Setup is installed via choco rather than assumed present on the runner image.

Every unit of work is a `claude -p` subprocess, so the tool cannot run a
campaign without the Claude Code CLI on PATH. That is not bundled -- Claude
Code is separately versioned and self-updating -- so the installer's finish
page and the README both name it, give the install command, and say to sign in
before first use. Neither claims anything about how that login is provisioned;
the runner inherits whatever the CLI already has.

The build is not code-signed and the README says so plainly, SmartScreen box
included, with `pip install .` offered as the no-installer path.
@joepetjr
joepetjr merged commit 6002f84 into main Aug 3, 2026
3 checks passed
@joepetjr
joepetjr deleted the feat/windows-installer branch August 3, 2026 22:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant