feat(ENG-12826): build standalone CLI binaries with PyInstaller + clean-room smoketest#318
Conversation
…ketest Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the linux-x86_64-gnu-only narrowing on PR events so every push exercises the full build + clean-room test matrix across all targets. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
⚠️ Not ready to approve
The PyInstaller spec currently references entry.py in a way that will fail when the workflow runs PyInstaller from the repo root, breaking binary builds.
Pull request overview
Adds a PyInstaller-based standalone distribution path for the Cloudsmith CLI, plus CI automation to build and clean-room smoketest those binaries across supported platforms, and updates runtime launcher/config generation to behave correctly when running from a frozen executable.
Changes:
- Introduces PyInstaller onedir build assets (
packaging/pyinstaller/*) and an end-to-end smoketest script (packaging/smoketest.sh). - Adds a multi-platform GitHub Actions workflow to build, checksum, and clean-room test standalone archives (
.github/workflows/binaries.yml), plus related linting tweaks. - Updates MCP config generation and Docker credential-helper launcher installation to prefer
sys.executablewhen running frozen, with tests covering the frozen behavior.
File summaries
| File | Description |
|---|---|
| packaging/smoketest.sh | Adds offline/online smoketest coverage for frozen binaries (imports, help/version sweeps, launcher self-reference checks). |
| packaging/pyinstaller/entry.py | Defines the frozen entrypoint and opt-in self-import sweep (CLOUDSMITH_SELFTEST) and UTF-8 stdio handling. |
| packaging/pyinstaller/cloudsmith.spec | PyInstaller onedir spec collecting CLI data, metadata, and hidden imports for binary builds. |
| packaging/constraints.txt | Adds a locked constraints export used to reproduce the binary build environment. |
| cloudsmith_cli/credential_helpers/docker/installer.py | Makes docker credential-helper launchers target the frozen executable when applicable. |
| cloudsmith_cli/cli/tests/commands/test_mcp.py | Adds a unit test asserting frozen MCP server configs run the executable directly. |
| cloudsmith_cli/cli/tests/commands/test_credential_helper_install.py | Adds tests ensuring frozen installs generate launchers that exec the absolute binary path. |
| cloudsmith_cli/cli/commands/mcp.py | Updates MCP server config selection to use sys.executable when frozen. |
| .typos.toml | Extends typos allowlist for PyInstaller spec terminology (datas). |
| .pre-commit-config.yaml | Adds a pre-commit hook to keep packaging/constraints.txt synced with uv.lock. |
| .github/workflows/zizmor.yml | Adds workflow concurrency control. |
| .github/workflows/binaries.yml | New build + clean-room test matrix workflow for standalone binaries across platforms/arches/libcs. |
Copilot's findings
- Files reviewed: 12/12 changed files
- Comments generated: 4
Note
Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- entry.py: add standard "# Copyright 2026 Cloudsmith Ltd" header (Copilot) — matches the convention used by other new modules. - cloudsmith.spec: add the same header after the mode/coding magic line (Copilot). - Declined (Copilot, spec lines 12 & 35): "Analysis(['entry.py']) will fail when PyInstaller runs from the repo root." PyInstaller resolves relative script paths in Analysis() against the spec file's directory (SPECPATH), not the CWD. Reproduced a full build from the repo root (the exact workflow invocation) — PyInstaller logged "Analyzing .../packaging/pyinstaller/entry.py" and the build completed successfully. No HERE/pathex change needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cloudsmith-iduffy
left a comment
There was a problem hiding this comment.
It would be good to get #317 merged and switch everything is this PR over to uv.
Review feedback on #318: move the inline wheel-content check heredoc to .github/scripts/check_wheel.py and the inline build-matrix JSON to .github/scripts/binaries-matrix.json, loaded with jq. Behavior unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review feedback on #318: with the uv migration (#317) in the base branch, install everything from the locked binary dependency group instead of pip plus an exported constraints file: - native/alpine/glibc builds now use uv sync --locked --no-dev --no-editable --group binary --extra all and uv run --no-sync pyinstaller. --no-editable matters: the default editable install leaves cloudsmith_cli out of site-packages, so PyInstaller cannot bundle it (the frozen binary fails with ModuleNotFoundError); a non-editable install matches the previous pip behavior. - container builds install a pinned uv (UV_VERSION) with UV_PYTHON_DOWNLOADS=never so they keep building against the image's system Python (preserving the glibc 2.28 floor on almalinux 8). - drop packaging/constraints.txt, its pre-commit sync hook, the CI export/cmp gate, and the AGENTS.md reference — uv.lock is now the single source of pins. Verified locally on macOS arm64: uv sync + uv run pyinstaller from the workflow commands, then --version and packaging/smoketest.sh offline against the built dist all pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
eng-12826-build-standalone-cli-binaries-with-pyinstaller-clean-room
Description
Adds standalone, no-Python-required CLI binaries built with PyInstaller, plus a clean-room smoketest workflow that builds and tests them across all target platforms.
What this PR adds:
packaging/pyinstaller/cloudsmith.spec+entry.py— PyInstaller onedir build (faster cold start than onefile; distributed as tar.gz / zip).entry.pyadds a frozen self-import sweep (CLOUDSMITH_SELFTEST) and UTF-8 stdout/stderr handling for legacy Windows consoles.packaging/constraints.txt— pinned, locked constraint set (exported fromuv.lock,--group binary --extra all) used to install the build environment reproducibly. Thebinariesworkflow verifies it matches the lockfile.packaging/smoketest.sh— offline + online (read-only) smoketests: import sweep,--version, per-command--helpsweep, keyring/auth path, AWS OIDC dep load, credential-helper docker offline + frozen-launcher self-reference, frozenmcp configure, and online whoami / list repos /mcp list_tools/ check service..github/workflows/binaries.yml— two-stage build → clean-room test matrix forlinux-{x86_64,aarch64}-{gnu,musl},macos-{arm64,x86_64},windows-x86_64. Builds in glibc (almalinux:8, glibc floor enforced <= 2.28) / musl (alpine) containers and native mac/Windows; tests download the artifact and run on clean runners that assert no Python is present. Includes a GitHub-OIDC online auth job.mcp.py_get_server_configandcredential_helpers/docker/installer.pynow detect a frozen (sys.frozen) binary and point the MCP/credential-helper launchers at the absolutesys.executableinstead of a barecloudsmithonPATH. Tests updated accordingly..github/workflows/zizmor.yml,.typos.toml— lint coverage for the new workflow.Type of Change
Additional Notes
Stacks on eng-12825 (uv + pyproject.toml packaging migration). Base:
eng-12825-migrate-cli-packaging-to-uv-pyprojecttomlat tip2e9e498.