Replace gh CLI dependency with GitHub App credentials in assistant-github#163
Replace gh CLI dependency with GitHub App credentials in assistant-github#163gregology[bot] wants to merge 7 commits intomainfrom
Conversation
…hema with GitHub App fields Two files change: 1. **`packages/assistant-github/pyproject.toml`** — add `PyJWT[crypto]` and `httpx` to the `dependencies` list alongside the existing `assistant-sdk>=0.1.0`. 2. **`packages/assistant-github/src/assistant_github/manifest.yaml`** — add four new properties to `config_schema.properties`: `github_user` (string), `app_id` (string), `installation_id` (string), `private_key` (string). All four should be listed in `config_schema.required`.
…ntication Rewrite **`packages/assistant-github/src/assistant_github/client.py`**: **Constructor** — accept `app_id: str`, `installation_id: str`, `private_key: str`, `github_user: str`. On init: 1. Create a JWT (RS256) with claims `iss=app_id`, `iat=now-60`, `exp=now+600` using `jwt.encode(payload, private_key, algorithm='RS256')`. 2. POST to `https://api.github.com/app/installations/{installation_id}/access_tokens` with `Authorization: Bearer <jwt>` to obtain an installation token. 3. Store the installation token and `github_user` as instance attributes. 4. Create an `httpx.Client` instance with base_url `https://api.github.com`, default headers `Authorization: Bearer <token>`, `Accept: application/vnd.github+json`, `X-GitHub-Api-Version: 2022-11-28`, and a reasonable timeout (30s). **Replace `_run_gh` and `_gh_api`** with a single `_request(method, url, *, params=None, json=None, headers=None)` method that uses `self._http.request(...)` with the existing retry/backoff logic (3 attempts, exponential backoff 1s/2s/4s). Raise on non-2xx after retries. Return parsed JSON (or raw text for diff endpoints). **Update public methods:** - `get_pr`, `get_pr_detail`, `get_issue`, `get_issue_detail` — call `_request('GET', f'/repos/{org}/{repo}/pulls/{number}')` etc. - `get_pr_diff` — call `_request('GET', ...)` with override header `Accept: application/vnd.github.v3.diff`, return raw text. - `create_issue` — call `_request('POST', f'/repos/{org}/{repo}/issues', json={'title': title, 'body': body})`. - `active_prs`, `active_issues` — replace all `@me` occurrences with `self._github_user` in search query strings. Use `_request('GET', '/search/issues', params={'q': query})` instead of `_gh_api`. - `_search_raw` — adapt to use `_request` with the search endpoint. **Remove** all `subprocess` imports and usage. Remove `_run_gh` and `_gh_api` methods entirely.
…etup wizard and doctor
**Handler call sites** — update all 5 files that instantiate `GitHubClient()` to pass credentials from the integration config:
- `platforms/pull_requests/check.py` (line 21)
- `platforms/pull_requests/collect.py` (line ~29)
- `platforms/issues/check.py` (line 21)
- `platforms/issues/collect.py` (line ~23)
- `services/create_issue.py` (line 38)
Each handler already receives an `integration` dict in its context/task. Extract `integration['app_id']`, `integration['installation_id']`, `integration['private_key']`, `integration['github_user']` and pass them to `GitHubClient(app_id=..., installation_id=..., private_key=..., github_user=...)`.
**`app/setup.py`** — in the `setup_github()` function:
- Remove the `gh` CLI detection and `gh auth status` check (lines ~212-226).
- Add prompts for: `github_user`, `app_id`, `installation_id`, and `private_key` (accept a path to a `.pem` file and read its contents).
- Store `private_key` in the secrets dict (matching the email pattern: `secrets[f'{name}_github_private_key'] = private_key_contents`). Store `app_id` and `installation_id` in secrets too.
- Return `(integrations, secrets)` tuple like `setup_email()` does (currently returns only integrations).
- Add `github_user` directly in the integration config dict.
- Use `!secret` references for the three credential fields in the integration config.
**`app/doctor.py`** — in `check_gh()` (lines ~111-128):
- Replace the gh CLI installed/authenticated check with a check that loads GitHub integration config, extracts app credentials, creates a JWT, and verifies the installation token endpoint returns 2xx.
- Update the diagnostic output messages accordingly.
**Tests** — update `packages/assistant-github/tests/test_client.py`: - `GitHubClient` instantiation in tests needs mocking since the constructor now calls the GitHub API for an installation token. Mock `httpx.Client.post` (or patch `_request`) during construction to return a fake token. - Replace all `@patch` targets from `_gh_api`/`_run_gh` to the new `_request` method. - Existing test assertions for parsing logic (get_pr status derivation, get_issue labels, create_issue return values, _scope_qualifiers, _search_entities deduplication) should remain largely the same — just update the mock surface. - Add new tests: (a) `github_user` substitution — verify that `active_prs` and `active_issues` build queries with the configured username instead of `@me`, (b) JWT generation — verify a JWT is created with correct claims (iss, iat, exp) and RS256 algorithm, (c) constructor calls the installation token endpoint. **Documentation:** - `packages/assistant-github/AGENTS.md` — remove references to `gh` CLI as the API client. Document that the client uses httpx with GitHub App installation tokens. Update the authentication section. - `example.config.yaml` — add a GitHub integration example block with `github_user`, `app_id: !secret github_app_id`, `installation_id: !secret github_installation_id`, `private_key: !secret github_private_key`. - `example.secrets.yaml` — add `github_app_id`, `github_installation_id`, `github_private_key` entries. - `README.md` — update any GitHub setup instructions to reference GitHub App credentials instead of `gh auth login`.
gregology
left a comment
There was a problem hiding this comment.
Why are we including github_secrets details in app/setup.py? These are concerns of the assistant-github package and it looks like we're tightly coupling that logic into the core app instead of respecting logical boundaries of what belongs in the app and what belongs in the packages
|
Good call — moved the GitHub credential prompting into The core setup wizard now iterates over discovered manifests, calls any declared setup hooks with a prompts adapter, and merges the results. No GitHub-specific knowledge left in Developer metricsTotal duration: 7m 37s
|
Why
Actions taken by the assistant (creating issues, eventually commenting on PRs) were showing up as the authenticated user because we shelled out to
gh apiand inherited whatevergh auth loginsession was active. Now that the integration creates issues on the user's behalf, it needs its own identity so there's a clear distinction between "the human did this" and "the bot did this."GitHub App installation tokens solve this cleanly — the assistant authenticates as the App, and GitHub shows the App's identity on anything it creates.
The secondary motivation: removing the hard runtime dependency on
ghbeing installed and authenticated. One less thing to go wrong during setup, one less thing fordoctorto check.Approach
Replaced all
subprocess.run(["gh", "api", ...])calls with direct HTTP requests viahttpx. Authentication uses GitHub App credentials: generate a JWT (RS256, via PyJWT), exchange it for an installation token, use that token for all API calls. Fresh token on everyGitHubClientinstantiation — no caching needed given the 12h schedule interval.The
@meshorthand in search queries (aghCLI convenience the raw API doesn't support) is replaced with an explicitgithub_userconfig field.Alternatives considered
Keep
ghCLI, just switch auth —ghsupports authenticating as a GitHub App, but it's awkward (environment variables, no clean token lifecycle control). Doesn't actually remove the runtime dependency, which was half the point.PyGithub — full-featured but heavy. We only use a handful of endpoints.
httpxis a better fit since we already know exactly which API calls we need, and it gives us async support down the road if we want it.Token caching with refresh — Installation tokens last 1 hour, but we only run every 12 hours. Generating a fresh token each time is simpler and avoids a whole class of stale-token bugs.
Review walkthrough
Each commit maps to one logical step:
Add dependencies and update manifest schema —
pyproject.tomlgetshttpxandPyJWT[crypto].manifest.yamlgets four new required config fields (github_user,app_id,installation_id,private_key).Rewrite GitHubClient to use httpx + GitHub App auth — The core change.
_run_gh/_gh_apiare gone, replaced by_requestwhich useshttpx.Clientwith retry/backoff. Constructor takes credentials, generates JWT, fetches installation token.@mereplaced withself._github_userin all search queries.Thread credentials to all GitHubClient call sites — Every handler that instantiates
GitHubClient()now passes credentials from the integration config. Follows the same# type: ignore[attr-defined]pattern the email integration uses (dynamic config models from manifest schema).Update setup wizard and doctor —
setup.pydrops theghCLI detection, prompts for App credentials andgithub_userinstead, writes secrets via!secretreferences.doctor.pyreplaces theghinstalled/authenticated check with a check that the App credentials can generate a valid installation token.Update tests — Mock surface changed from
_gh_api/_run_ghto_request(and_generate_jwt/_fetch_installation_tokenduring construction). Added tests for JWT generation,github_usersubstitution in search queries, and retry logic.Update documentation and examples —
AGENTS.md,AGENTS.yaml,README.md,example.config.yaml,example.secrets.yamlall updated to reflect the new auth mechanism.Closes #162
Developer metrics
Total duration: 23m 16s
Turns: 428
Tool calls: 336
Tokens: 6,927,217 input / 54,095 output
Resolves #162