From e32980388f2783d11131a72e6887e9185c9ed990 Mon Sep 17 00:00:00 2001 From: Alex Kroman Date: Mon, 8 Jun 2026 09:55:14 -0700 Subject: [PATCH] Test brew/pipx/uv tool installs from the branch in CI Lead the README with Homebrew (recommended) via `--HEAD` (works from the private repo today), then pipx and `uv tool install`, demoting the curl|sh one-liner to last. Back the documented install paths with real CI coverage built from the branch's own code: - uv tool: new `test_install_via_uv_tool` installs the branch wheel into a hermetic UV_TOOL_* home; the existing macOS matrix widens to `pipx or uv_tool`. pipx is already exercised by install.sh's `pipx install ` branch. - brew: new macOS-only `formula-install` job repoints the formula at a `git archive` tarball of the CI checkout with a computed sha256, installs it through a throwaway `--no-git` tap, and runs `aai version` + `brew test`. This sidesteps the placeholder sha that blocked real installs until the v0.1.0 tag, and exercises the formula's resource list (the part most likely to drift from pyproject/uv.lock). Also add a `head` spec to the formula so `brew install --HEAD aai` works pre-release. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 69 ++++++++++++++++++++++++++---- Formula/aai.rb | 1 + README.md | 26 +++++++---- pyproject.toml | 2 +- tests/test_install_script_smoke.py | 47 ++++++++++++++++---- 5 files changed, 120 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be4de5ae..fc7dfc49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,10 +111,10 @@ jobs: # `brew style` is the RuboCop-based formula linter and runs fully offline, so # it lints idioms (resource/depends_on ordering, on_linux scoping) on every PR. - # The stricter `brew audit --strict --online` and a real `brew install`/`brew - # test` build are deliberately NOT here: they need the source `sha256`, which - # stays a placeholder until the v0.1.0 release tag is cut. Those belong in a - # release-time job (see the Homebrew tap plan, Task 4/6). + # A real `brew install` + `brew test` build runs in the `formula-install` job + # below (it repoints the formula at a branch-source tarball with a computed + # sha256, sidestepping the placeholder that stays until the v0.1.0 tag is cut). + # The stricter `brew audit --strict --online` still belongs in a release job. - name: Lint the formula run: brew style ./Formula/aai.rb @@ -189,15 +189,15 @@ jobs: python -m pip_audit install-smoke: - name: install.sh real install (${{ matrix.os }}) + name: package install — real (${{ matrix.os }}) strategy: fail-fast: false matrix: include: - os: ubuntu-latest - kfilter: "" # both branches: pipx + pip --user + kfilter: "" # all branches: pipx + pip --user + uv tool - os: macos-latest - kfilter: "-k pipx" # pipx only — PEP 668 makes pip --user flaky on macOS + kfilter: '-k "pipx or uv_tool"' # pip --user is flaky on macOS (PEP 668) runs-on: ${{ matrix.os }} timeout-minutes: 15 steps: @@ -220,7 +220,8 @@ jobs: # Use the system interpreter (no virtualenv) so install.sh's `pip --user` # branch is allowed. Editable install makes `aai_cli` importable for the - # test's __version__ check; uv builds the wheel; pipx drives the pipx branch. + # test's __version__ check; uv builds the wheel and drives the uv tool + # branch; pipx drives the pipx branch (which doubles as `pipx install`). - name: Tooling run: | python -m pip install --upgrade pip # need pip >= 25.1 for --group @@ -228,3 +229,55 @@ jobs: - name: Real install smoke run: python -m pytest -q -m install_script ${{ matrix.kfilter }} + + formula-install: + # Real `brew install` of the Homebrew formula, built from THIS branch's source. + # macOS only: that's where the README points brew users first, and where the + # macOS keyring backend (no on_linux jeepney/secretstorage) actually runs. + name: brew install (real, macOS) + runs-on: macos-latest + timeout-minutes: 40 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false # no job pushes; don't leave the token in .git/config + - uses: Homebrew/actions/setup-homebrew@2ebcf16054461267868620b1414507f3ccc765c1 + + # The committed formula's stable `url` points at a not-yet-cut release tag + # with a placeholder sha256, so a vanilla install can't build the branch. + # Repoint it at a source tarball of THIS checkout (the merge commit CI runs + # on) with the real checksum, so `brew install` compiles the branch through + # the formula's own resource list — the part most likely to drift from + # pyproject/uv.lock. count=1 keeps every resource url/sha256 untouched. + - name: Pin the formula to the branch source + run: | + set -euo pipefail + version="$(grep -m1 '^version' pyproject.toml | sed -E 's/.*"([^"]+)".*/\1/')" + tarball="$RUNNER_TEMP/aai-${version}.tar.gz" + # Mirror a GitHub archive: a single top-level dir brew strips on extract. + git archive --format=tar.gz --prefix="aai-${version}/" -o "$tarball" HEAD + sha="$(shasum -a 256 "$tarball" | awk '{print $1}')" + python3 - "Formula/aai.rb" "$tarball" "$sha" <<'PY' + import re, sys, pathlib + formula, tarball, sha = sys.argv[1], sys.argv[2], sys.argv[3] + p = pathlib.Path(formula) + src = p.read_text() + src = re.sub(r'url ".*?"', f'url "file://{tarball}"', src, count=1) + src = re.sub(r"sha256 .*", f'sha256 "{sha}"', src, count=1) + p.write_text(src) + PY + grep -nE '^ (url|sha256) ' Formula/aai.rb | head -2 + + # Newer Homebrew refuses formulae outside a tap, so install through a + # throwaway local tap holding the pinned formula (--no-git avoids needing a + # git identity on the runner). --build-from-source compiles the full resource + # list (rust + cryptography native builds); `brew test` then runs the + # formula's own `test do` block, asserting `aai version`. + - name: brew install + test + run: | + set -euo pipefail + brew tap-new --no-git aai/local + cp Formula/aai.rb "$(brew --repository aai/local)/Formula/aai.rb" + brew install --build-from-source --formula aai/local/aai + aai version + brew test --formula aai/local/aai diff --git a/Formula/aai.rb b/Formula/aai.rb index 7b88c340..2017fe25 100644 --- a/Formula/aai.rb +++ b/Formula/aai.rb @@ -6,6 +6,7 @@ class Aai < Formula url "https://github.com/AssemblyAI/cli/archive/refs/tags/v0.1.0.tar.gz" sha256 "0" * 64 # FILLED IN by the release cut (plan Task 2) once the v0.1.0 tag exists license "MIT" + head "https://github.com/AssemblyAI/cli.git", branch: "main" depends_on "pkgconf" => :build # cffi / cryptography native builds depends_on "rust" => :build # pydantic-core, jiter, cryptography diff --git a/README.md b/README.md index 6f6807cd..98164eaa 100644 --- a/README.md +++ b/README.md @@ -27,24 +27,34 @@ ## Installation +### Homebrew (recommended — macOS / Linux) + ```sh -# YOLO -curl -fsSL https://raw.githubusercontent.com/AssemblyAI/cli/main/install.sh | sh +brew tap assemblyai/cli https://github.com/AssemblyAI/cli +brew install --HEAD aai +``` + +`brew install` pulls in `ffmpeg` and `portaudio` for you, so `transcribe`, `stream`, and `agent` work out of the box. While the repo is private and pre-release, `--HEAD` builds from the latest `main` (the tap clone uses your existing GitHub credentials); once the first release is tagged, drop the flag and just `brew install aai`. Upgrade with `brew upgrade --fetch-HEAD aai`; remove with `brew uninstall aai`. -# pipx (recommended) +### pipx / uv + +```sh +# pipx pipx install "git+https://github.com/AssemblyAI/cli.git" + +# uv +uv tool install "git+https://github.com/AssemblyAI/cli.git" ``` -Requires Python 3.12+. The installer prefers [`pipx`](https://pipx.pypa.io), falling back to `pip --user`. Microphone and speaker support (for `stream` and `agent`) is included by default via [`sounddevice`](https://python-sounddevice.readthedocs.io) — its macOS and Windows wheels bundle PortAudio. On Linux, install the runtime once: `sudo apt-get install libportaudio2`. +Requires Python 3.12+. Microphone and speaker support (for `stream` and `agent`) is included by default via [`sounddevice`](https://python-sounddevice.readthedocs.io) — its macOS and Windows wheels bundle PortAudio. On Linux, install the runtime once: `sudo apt-get install libportaudio2`. You'll also want [`ffmpeg`](https://ffmpeg.org) on `PATH` to decode non-WAV/URL audio. -### Homebrew (macOS / Linux) +### One-liner ```sh -brew tap assemblyai/cli https://github.com/AssemblyAI/cli -brew install aai +curl -fsSL https://raw.githubusercontent.com/AssemblyAI/cli/main/install.sh | sh ``` -`brew install` pulls in `ffmpeg` and `portaudio` for you, so `transcribe`, `stream`, and `agent` work out of the box. Upgrade with `brew upgrade aai`; remove with `brew uninstall aai`. +Prefers [`pipx`](https://pipx.pypa.io), falling back to `pip --user`. ## Quick Start diff --git a/pyproject.toml b/pyproject.toml index 86e5f57f..a3d40a58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,7 +111,7 @@ filterwarnings = [ ] markers = [ "e2e: real-API end-to-end tests that drive the CLI (need ASSEMBLYAI_API_KEY; skip otherwise)", - "install_script: real install via install.sh from a locally-built wheel; asserts `aai` runs (slow; needs network + uv/pipx; skip otherwise)", + "install_script: real install of a locally-built wheel via install.sh (pipx / pip --user) and `uv tool install`; asserts `aai` runs (slow; needs network + uv/pipx; skip otherwise)", "install: install each init template's requirements.txt into a clean venv and import it (slow; needs network + uv; skip otherwise)", ] diff --git a/tests/test_install_script_smoke.py b/tests/test_install_script_smoke.py index e811f21f..caa04dfa 100644 --- a/tests/test_install_script_smoke.py +++ b/tests/test_install_script_smoke.py @@ -1,18 +1,25 @@ -"""Real install-and-run smoke test for install.sh. +"""Real install-and-run smoke tests for the documented install paths. -Builds a wheel from the checkout and runs install.sh against it (via the -test-only AAI_SPEC override) into a hermetic location, then asserts the -installed `aai` binary actually runs. This is the one check that exercises the -public install path end to end: dependency resolution, the console entrypoint, -and the pipx / pip --user branches in install.sh. +Builds a wheel from the checkout once, then installs it into hermetic locations +and asserts the resulting `aai` binary actually runs — exercising dependency +resolution + the console entrypoint for each documented installer: + +* install.sh's pipx and pip --user branches (via the test-only AAI_SPEC + override, which installs the wheel instead of the public git URL so tests + need no push); install.sh's pipx branch is literally `pipx install `, + so it doubles as coverage of the README's `pipx install` command. +* `uv tool install ` — the README's uv path. + +The Homebrew path is covered separately by the `formula-install` CI job, which +does a real `brew install` of the formula built against the branch source. Marked `install_script`: slow + needs network (deps resolve from PyPI) and pipx/uv. Excluded from the default run; invoke explicitly:: uv run pytest -m install_script -The two tests map to the two install branches; CI runs both on Linux and only -the pipx branch on macOS. +CI runs every branch on Linux; on macOS it runs the pipx + uv tool branches +(`pip --user` is flaky there under PEP 668). """ from __future__ import annotations @@ -115,3 +122,27 @@ def test_install_via_pip_user(built_wheel: Path, tmp_path: Path) -> None: run = subprocess.run([_sh(), str(INSTALL_SH)], env=env, capture_output=True, text=True) assert run.returncode == 0, run.stderr _assert_aai_runs(userbase / "bin" / "aai") + + +def test_install_via_uv_tool(built_wheel: Path, tmp_path: Path) -> None: + if shutil.which("uv") is None: + pytest.skip("uv not on PATH; required for the uv tool install branch") + if not _pypi_reachable(): + pytest.skip("PyPI unreachable; skipping real-install smoke test (offline)") + + # Hermetic uv tool dirs so the install never touches the developer's real + # toolchain; UV_TOOL_BIN_DIR is where uv links the `aai` entrypoint. + bindir = tmp_path / "uv_bin" + env = { + **os.environ, + "UV_TOOL_DIR": str(tmp_path / "uv_tools"), + "UV_TOOL_BIN_DIR": str(bindir), + } + run = subprocess.run( + ["uv", "tool", "install", str(built_wheel)], + env=env, + capture_output=True, + text=True, + ) + assert run.returncode == 0, run.stderr + _assert_aai_runs(bindir / "aai")