feat: add Homebrew tap support with release auto-bump and plugin Python ABI guard#42
Merged
lukeocodes merged 6 commits intomainfrom Apr 30, 2026
Merged
Conversation
Wires the new deepgram/homebrew-tap/Formula/deepgram.rb to regenerate
itself on every root v* release of deepctl.
Pieces:
- scripts/templates/deepgram.rb.template: source of truth for the formula.
Contains the same hand-authored header / install / test as what's now in
the tap, plus {{TARBALL_URL}} / {{TARBALL_SHA256}} / {{RESOURCES}}
placeholders.
- scripts/bump_brew_formula.py: fetches PyPI metadata, runs
homebrew-pypi-poet inside an isolated venv (with setuptools<80 to work
around the pkg_resources removal in setuptools 80+), extracts only the
resource blocks, and substitutes them into the template. Output is
deterministic per (version, python_exe). mypy + ruff clean.
- .github/workflows/release.yml: adds a bump-brew-formula job gated on
root v* tags only (sub-package tags like deepctl-cmd-listen-v0.0.3 are
skipped, matching the existing mark-latest / deploy-web pattern). The
job waits for the new version to appear on PyPI, mints a short-lived
installation token via actions/create-github-app-token, checks out
homebrew-tap, regenerates the formula, and opens a bump PR.
- docs/HOMEBREW_AUTOMATION.md: how the system fits together, the GitHub
App security spec, credential names, manual/emergency bump steps, key
rotation, and operational notes.
Verified locally: running scripts/bump_brew_formula.py --version 0.2.18
--dry-run produces a byte-identical formula to what's in the open
deepgram/homebrew-tap PR.
The job will fail until the GitHub App is created and the
HOMEBREW_BUMP_APP_ID variable + HOMEBREW_BUMP_APP_KEY secret are populated
in this repo's settings; the rest of the release pipeline is unaffected.
This was referenced Apr 29, 2026
The bump script (scripts/bump_brew_formula.py), template, and workflow job in release.yml are self-documenting: - The script's --help output covers manual / emergency usage - The workflow's job step names + comments cover the operational flow - The auto-generated formula's banner points back to the template The GitHub App setup spec lives outside this repo (in the security team's intake — Slack / Notion / their tracker of choice). No reason to mirror it here.
When a user installs deepctl via Homebrew (or any path that ends up using
IsolatedVenvStrategy), then later upgrades the underlying interpreter
(e.g. brew upgrade python@3.13), the running interpreter no longer
matches the Python that built ~/.deepctl/plugins/venv/. Pure-Python
plugins keep loading because PluginManager bridges them via sys.path,
but C-extension plugins fail with a confusing low-level ImportError that
gives users no obvious remediation.
This adds a one-line warning at plugin-load time that surfaces the
mismatch and tells the user how to rebuild:
Plugin environment was built with Python 3.12 but you're running
Python 3.13. C-extension plugins may fail to load. To rebuild:
rm -rf ~/.deepctl/plugins/venv && deepctl plugin install <plugin>
Implementation:
- New get_venv_python_version() in plugin_env.py reads pyvenv.cfg and
returns (major, minor). Handles both stdlib's 'version' key and uv's
'version_info' key. Returns None on missing/malformed cfg so callers
can treat 'unknown' as 'no warning'.
- New _warn_if_plugin_venv_python_mismatch() in PluginManager fires
before sys.path bridging in _load_plugin_venv_entries, only when
there's a real mismatch (no warning for unknown / matching versions).
Tests:
- 6 cases for get_venv_python_version (missing venv, missing cfg,
stdlib 'version' key, uv 'version_info' key, malformed, no version key)
- 4 cases for the warning logic (silent on unknown, silent on match,
warns on minor diff, warns on major diff)
All 48 plugin tests pass. mypy + ruff clean on changed src files.
Adds 'macOS / Linux (Homebrew)' as the first option under Quick Install: brew install deepgram/tap/deepgram Documents that Homebrew handles ffmpeg + portaudio automatically (covers dg listen --mic, dg debug probe, raw audio piping) and notes brew upgrade deepgram for updates. Companion to deepgram/homebrew-tap#1 (formula) and #42 (release-automation that keeps the formula fresh).
Switches the README's Homebrew snippet from the one-shot brew install deepgram/tap/deepgram to the two-step brew tap deepgram/tap brew install deepgram After tapping, future installs and upgrades use the bare formula name (brew upgrade deepgram), which is what users will most often see and type. The two-step form is also more explicit about what's being tapped.
Replaces the actions/create-github-app-token step + APP_ID variable +
APP_KEY secret with a single fine-grained PAT scoped to deepgram/homebrew-tap
(Contents: Write, Pull requests: Write).
Workflow becomes shorter and removes the App-installation discovery step.
Trade-off: PRs are attributed to the PAT owner (a real GitHub user)
rather than a bot identity — which is fine since the PAT owner is the
explicit accountable party for these automated bumps.
Commit attribution uses the PAT owner's noreply email (looked up via
'gh api /user') instead of ${{ github.actor }} so commits show the
credential owner, not whoever triggered the release (which is typically
release-please[bot]).
Required repo secret: CLI_TAP_SYNC_PAT (already added).
lukeocodes
added a commit
that referenced
this pull request
May 1, 2026
## Summary
Restores the CI Lint job (\`uv run ruff format --check && ruff check &&
mypy --strict\` against \`src/\` + \`packages/*/src\`) to green. Was
failing on \`origin/main\` against pre-existing files, blocking every
PR's lint gate until either fixed or admin-overridden.
## What it does
| Fix | Why it was broken |
|---|---|
| \`ruff format\` 3 files | \`debug-toolkit/{command,fetcher}.py\` +
\`core/auth.py\` were committed unformatted (likely landed via [no-ci]
paths or admin merges) |
| Add \`deepgram_mcp\` + \`deepgram_mcp.*\` to the existing
\`ignore_missing_imports\` mypy override | The MCP SDK ships without
type stubs, so \`import deepgram_mcp\` produced \`import-untyped\`
errors. The existing override already covers \`sounddevice\`/\`numpy\`;
adding \`deepgram_mcp\` to the same list is the minimal fix. |
| Switch one import in \`mcp/models.py\` to \`from X import Y as Y\` |
The current \`from deepgram_mcp import TransportType\` + \`# noqa:
F401\` produced an \`attr-defined\` error when re-exported via
\`__init__.py\` under \`mypy --strict\`. The explicit \`as\` form is the
standard way to mark a re-export for strict mypy and drops the
now-redundant \`noqa\`. |
5 files, ~30 lines diff. **No logic changes** — pure config + format +
import-statement shape.
## Verification
```
uv run ruff format --check src/ packages/*/src # 112 files already formatted
uv run ruff check src/ packages/*/src # All checks passed!
uv run mypy src/ packages/*/src # Success: no issues found in 112 source files
```
These are the exact commands [CI's Lint
job](https://github.com/deepgram/cli/blob/main/.github/workflows/test.yml#L41-L65)
runs.
## Context
These two fix commits originally landed on the [\`#42\`
branch](#42) while it was open, but
#42 was admin-merged before the fix commits could be picked up. They got
stranded on the deleted branch. This PR brings them back as a single
small chore so future PRs aren't blocked.
This was referenced May 1, 2026
Closed
lukeocodes
added a commit
that referenced
this pull request
May 1, 2026
…47) ## Summary Two release-infrastructure fixes that need to land before [#46](#46) (release-please's `chore: release main`) is merged. Without this, `pip install deepctl==0.2.19` will **fail to resolve dependencies** for every user. ## What's broken ### 🔴 Install-blocker: `deepctl-cmd-debug-toolkit` is a phantom package The toolkit subcommand was added in [`768f34f`](768f34f) (`feat(debug): add toolkit subcommand for deepgram/support-toolkit scripts`). The commit: - ✅ Created `packages/deepctl-cmd-debug-toolkit/` with `pyproject.toml`, `src/`, etc. - ✅ Added `deepctl-cmd-debug-toolkit>=0.0.1` to the umbrella `pyproject.toml` dependencies - ✅ Added `[tool.uv.sources]` workspace mapping - ❌ **Did not register the package in [`release-please-config.json`](https://github.com/deepgram/cli/blob/main/.github/release-please-config.json)** - ❌ **Did not register the package in [`.release-please-manifest.json`](https://github.com/deepgram/cli/blob/main/.github/.release-please-manifest.json)** Result: the package has never been built or published. `pip install deepctl-cmd-debug-toolkit` returns `Not Found` on PyPI today. When `deepctl 0.2.19` publishes with `deepctl-cmd-debug-toolkit>=0.0.1` in its dependency list, pip will try to resolve that and fail with a missing-distribution error. [AGENTS.md step 6](https://github.com/deepgram/cli/blob/main/AGENTS.md#6-wire-into-the-workspace) calls out exactly this set of files as the workspace integration checklist for new packages — it just wasn't followed. ### 🟡 deepctl-core's plugin warning never reaches PyPI users [`fb22d8a`](fb22d8a) (the squash merge of #42) added `_warn_if_plugin_venv_python_mismatch()` to `deepctl-core`'s `PluginManager`. But its squash-merge body included markdown tables and `## Summary` / `## What lands` headings, and `conventional-commits-parser` choked on it. The release-please run logs show: ``` ❯ commit could not be parsed: fb22d8a feat(homebrew): homebrew tap support (#42) ``` release-please skipped the commit entirely. So deepctl-core stayed at **0.2.8** in the manifest, no bump, no PyPI release. The umbrella `deepctl 0.2.19` will publish with `deepctl-core>=0.1.10` which pip resolves to PyPI's 0.2.8 — **the version before the plugin warning was added**. The warning code is on `main` but no PyPI user gets it. ## Fixes in this PR 1. **Register `deepctl-cmd-debug-toolkit` with release-please.** - `.github/release-please-config.json` — add the package block (matches the convention every other sub-package uses) - `.github/.release-please-manifest.json` — add `\"packages/deepctl-cmd-debug-toolkit\": \"0.0.0\"` so release-please's first release bumps it to `0.0.1` (matches `pyproject.toml`) 2. **Trigger deepctl-core release with a parseable conventional commit.** Tweaks the plugin warning's remediation hint from `deepctl plugin install` to `dg plugin install` — the canonical user-facing alias used everywhere else in user messages and docs. Small real improvement. The squash-merge title `feat(deepctl-core): ...` parses cleanly and gives release-please a path-attributable feat on `packages/deepctl-core/*` to detect. 3. **Regenerate stale READMEs.** `scripts/generate_readmes.py --check` flagged 3 stale READMEs on `main` (root, `deepctl-cmd-listen`, `deepctl-cmd-mcp`) plus the missing `deepctl-cmd-debug-toolkit/README.md`. All four regenerated. ## After this merges release-please will recompute and PR #46's body will look like: ``` <details><summary>0.2.19</summary> # deepctl root <details><summary>deepctl-cmd-debug-toolkit: 0.0.1</summary> # NEW — fixes the install <details><summary>deepctl-core: 0.2.9</summary> # NEW — ships the plugin warning ``` Merging #46 then publishes all three to PyPI in one release event, and `pip install deepctl==0.2.19` resolves end-to-end. ## Verification - `ruff format --check src/ packages/*/src` — 112 files already formatted - `ruff check src/ packages/*/src` — clean - `mypy --strict src/ packages/*/src` — clean (112 source files) - `python scripts/generate_readmes.py --check` — all current - 4 plugin-warning tests still pass (test message regex doesn't depend on `deepctl` vs `dg`) ## Pre-existing comments / docstrings A couple of necessary comments still in this PR were retained per project convention: - `_warn_if_plugin_venv_python_mismatch` docstring — Category 3, documents the *when* (Homebrew Python upgrade scenario), the *why-warn-instead-of-block* contract, and *None semantics*. Same justification as in #42. - The text change is one character (`deepctl` → `dg`) and a minor improvement, not a behavioral change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds first-party Homebrew tap support for the Deepgram CLI. Three pieces of work that share the same lifecycle:
brew tap deepgram/tap+brew install deepgramto "Quick Install" with a note that Homebrew handlesffmpeg+portaudioautomatically.bump-brew-formulajob inrelease.yml(gated on rootv*tags only) that regeneratesFormula/deepgram.rbon every CLI release usinghomebrew-pypi-poet, then opens a bump PR back againstdeepgram/homebrew-tap. Uses a fine-grained PAT (CLI_TAP_SYNC_PAT) scoped tohomebrew-taponly (Contents: Write, Pull requests: Write) for cross-repo auth.IsolatedVenvStrategy, so plugins survive the install path by design. This adds the last edge case: whenbrew upgrade python@3.13rebuilds the CLI against a newer Python without users recreating their plugin venv, surface a one-line warning with explicit remediation. Pure-Python plugins keep loading transparently; only C-extension plugins were affected.What lands
README.mdbrew tap+brew install deepgram)scripts/templates/deepgram.rb.templatescripts/bump_brew_formula.pyhomebrew-pypi-poetoutput → renders formula.github/workflows/release.ymlbump-brew-formulajob gated on rootv*tags onlydeepctl-cmd-listen-v0.0.3are skipped (matches existingmark-latest/deploy-webgating)packages/deepctl-core/src/deepctl_core/plugin_env.pyget_venv_python_version()readspyvenv.cfg, returns(major, minor)orNoneversionkey and uv-styleversion_infokeypackages/deepctl-core/src/deepctl_core/plugin_manager.py_warn_if_plugin_venv_python_mismatch()fires inside_load_plugin_venv_entriessys.pathbridging; only the warning is newpackages/deepctl-core/tests/unit/test_plugin_env.pyget_venv_python_versionpackages/deepctl-core/tests/unit/test_plugin_manager.pyCross-repo auth
GITHUB_TOKENis repo-scoped and cannot push todeepgram/homebrew-tap. We're using a fine-grained PAT —CLI_TAP_SYNC_PAT— scoped todeepgram/homebrew-taponly with the minimum permissions required:Stored as a repo secret in
deepgram/cli. Already created and added.Commit attribution: the bump commit's author is set to the PAT owner (looked up via
gh api /user) so the homebrew-tap PR has a clear, linkable author. Using${{ github.actor }}would misattribute torelease-please[bot](the workflow trigger), not the credential owner.Trade-off vs an org-owned GitHub App: the PAT is bound to a real user, so if that user rotates or revokes the token the workflow breaks until a new PAT is provisioned. Easy to migrate to an App later — the workflow's only secret reference is
secrets.CLI_TAP_SYNC_PAT.Verification
pytest packages/deepctl-core/tests/unit/test_plugin_*.py -vruff check(changed src files)ruff format --check(changed src files)mypy --strict(changed src files)python -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml'))"scripts/bump_brew_formula.py --version 0.2.18 --dry-runproduces outputdiff-equal to the formula indeepgram/homebrew-tap#1(post-banner)The first auto-bump after this lands will produce a clean version-only diff, not a churn diff.
What only a real release can validate
gh pr createfrom the runner correctly attributing the bump PR to the PAT ownerIf any misfires on first run, the rest of the release pipeline is unaffected — the failure surface is contained to the bump job.
Companion PRs
deepgram/homebrew-tap#1— the formula itselfdeepgram/deepgram-docs#834— install-page content + sidebar restructure (consolidating into one PR)Pre-existing comments / docstrings
Several comments in the new code were retained as Category 3 necessary:
setuptools<80pin rationale in the bump script (prevents "tidy up pinned deps" PRs that would silently break it)extract_resource_blocksrationale (documents the template-driven design vs naively using poet's full output)_DG_COMPLETErationale in the template (prevents future "simplifications" that would silently break tab-completion fordg)get_venv_python_versionand_warn_if_plugin_venv_python_mismatchdocstrings (document the dualversion/version_infokey support, theNonecontract, and why we warn instead of block)Each describes a behavioral contract that the code alone can't communicate; removing them would predictably lead to wrong-direction refactors.