Skip to content

feat: multi-account support for github.com#158

Open
dokun1 wants to merge 43 commits intogharlan:mainfrom
dokun1:feat/multi-account
Open

feat: multi-account support for github.com#158
dokun1 wants to merge 43 commits intogharlan:mainfrom
dokun1:feat/multi-account

Conversation

@dokun1
Copy link
Copy Markdown

@dokun1 dokun1 commented Apr 12, 2026

Summary

Adds multi-account support for github.com — users can register multiple GitHub accounts and switch between them.

Important

This PR depends on #157 (tests/add-phpunit-harness). Please merge that first — this branch is stacked on top of it. Once #157 is merged, I'll rebase this PR onto main and update the base branch.

What it does

  • gh > user add <label> — starts the OAuth flow with login=<label>&prompt=select_account so GitHub shows the account picker. The label is stored locally; server.php saves the token under that label when the callback completes. If no explicit label is pending (legacy gh > login), the callback resolves the actual GitHub username via the /user API.
  • gh > user switch — lists all registered accounts; selecting one switches the active account and shows a macOS alert dialog with an "Open GitHub" button to sync the browser session.
  • gh > user switch <label> — direct switch by name.
  • gh > user update <label> — re-runs OAuth to refresh a token.
  • gh > user delete <label> — shows a confirmation dialog, then removes the account and its cached data. Errors if the account is currently active.
  • gh > user login <label> <token> — manual PAT fallback for cases where OAuth doesn't work.

What stays the same

  • All existing commands (gh my pulls, gh > login, gh > logout, gh > delete cache, ghe > url, ghe > login, etc.) work identically.
  • Enterprise mode is completely untouched. All ghe > commands use the same config-based token path as before.
  • Single-account users see no difference. On upgrade, the existing access_token is automatically migrated to an account labeled with the user's GitHub username. Everything keeps working.

Behavioral changes to note

  • gh > delete database now clears request_cache and config tables but preserves the accounts table, so users don't have to re-authorize every account on a cache reset. Previously it deleted the entire SQLite file.
  • gh > logout clears the active account's token but preserves the account row, so re-login keeps the label.
  • The header item (the gh / ghe prompt shown when the Alfred bar is empty) now shows gh (<username>) to indicate which account is active.
  • Security fix (pre-existing): exec('open '.$query) in action.php now uses escapeshellarg() to prevent command injection from crafted URLs.

Architecture

Schema

A new accounts table holds credentials:

CREATE TABLE accounts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    label TEXT NOT NULL UNIQUE,
    token TEXT NOT NULL,
    is_active INTEGER NOT NULL DEFAULT 0,
    created_at INTEGER NOT NULL
);
CREATE UNIQUE INDEX accounts_one_active ON accounts(is_active) WHERE is_active = 1;

request_cache gains an account_id column with a rebuilt composite primary key (account_id, url) so cached API responses are partitioned per account. Enterprise cache uses sentinel account_id = 0.

Upgrade path

On first init() after upgrade:

  1. ensureAccountsTable() creates the accounts table idempotently (CREATE TABLE IF NOT EXISTS).
  2. migrateRequestCacheSchema() detects the missing account_id column via PRAGMA table_info and rebuilds the table inside a transaction.
  3. migrateLegacyAccessToken() seeds the existing access_token config value into an active account row.

All three steps are idempotent and safe to run on every invocation.

Files changed (production)

File Change
workflow.php Accounts CRUD, migration, cache partitioning, token accessor rewrites
action.php Extracted Action::dispatch() for testability; added user subcommand routing; macOS alert dialogs
search.php Header shows active label; > user switch/delete/update/add/login render account lists
server.php OAuth callback reads pending label from config, resolves username via API

Files unchanged

curl.php, item.php, info.plist, .php-cs-fixer.dist.php — zero modifications.

Test plan

110 automated tests, 208 assertions (includes the 30 characterization tests from PR #157):

Test file What it covers
AccountsSchemaTest Table columns, constraints, NOT NULL, partial unique index
AccountsMigrationTest Legacy token migration, upgrade path for pre-existing databases, idempotency
RequestCachePartitionTest account_id column, composite PK, two-account non-collision, legacy DB migration, enterprise sentinel, cache resolver for both github and enterprise mode
AccountsCrudTest add/list/get/setActive/remove/updateToken, deleteDatabase preserves accounts
ActionDispatchTest All existing command routing + all user subcommand branches (add/login/switch/update/delete)
SearchHeaderTest Active account label rendering in the gh header
WorkflowTokenTest Token isolation (github vs enterprise), legacy login/logout compat, empty-string-as-null, account recovery
PR #1 characterization tests Config, schema, enterprise URLs, CurlRequest, Item::toXml

Manual QA performed: fresh install → login (label auto-resolved to GitHub username) → add second account via OAuth with account picker → switch (alert dialog) → verify API results change → delete (confirmation dialog) → re-add → logout/re-login → delete database (accounts survive).

  • vendor/bin/phpunit — 110 tests, 208 assertions, all green (PHP 8.5)
  • CI passes on PHP 8.2 / 8.3 / 8.4
  • Maintainer review

🤖 Generated with Claude Code

dokun1 and others added 30 commits April 11, 2026 14:05
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 12, 2026 19:24
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds multi-account support for github.com by introducing an accounts table, migrating legacy single-token storage, and partitioning the request cache per active account (while keeping GitHub Enterprise on the legacy token path). The PR also adds a PHPUnit harness + CI coverage to prevent regressions across the new migration and routing behavior.

Changes:

  • Introduce accounts storage + active-account switching, and migrate legacy access_token on init.
  • Partition request_cache by account_id (enterprise uses sentinel 0) and migrate legacy cache schema.
  • Add > user ... commands (add/login/switch/update/delete), update OAuth callback handling, and add PHPUnit tests + GitHub Actions CI.

Reviewed changes

Copilot reviewed 21 out of 22 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
workflow.php Adds accounts CRUD + legacy migrations; partitions request cache by account.
action.php Refactors into Action::dispatch(), adds user subcommand handlers, and hardens open exec quoting.
search.php Adds > user ... command rendering and shows active account in the gh header.
server.php OAuth callback now stores tokens under a label and can resolve username via /user.
composer.json Adds PHPUnit dev dependency and composer test script.
phpunit.xml.dist PHPUnit configuration and bootstrap wiring.
.github/workflows/test.yml CI matrix running PHPUnit on PHP 8.2–8.4.
.gitignore Ignores PHPUnit cache and local composer artifacts.
tests/bootstrap.php Test bootstrap for loading workflow files and resetting static state.
tests/WorkflowTestCase.php Base test case for isolated temp data dirs + Workflow static reset.
tests/WorkflowTokenTest.php Tests token isolation and multi-account token behaviors.
tests/WorkflowSchemaTest.php Pins schema shape and init idempotency.
tests/WorkflowEnterpriseUrlTest.php Pins enterprise URL derivation behavior.
tests/WorkflowConfigTest.php Pins config CRUD behavior.
tests/SearchHeaderTest.php Verifies header includes active account label.
tests/RequestCachePartitionTest.php Verifies cache partitioning + legacy migration.
tests/ItemRenderTest.php Characterization tests for Item::toXml.
tests/CurlRequestTest.php Pins CurlRequest value-object behavior.
tests/ActionDispatchTest.php Covers command routing including new user subcommands.
tests/AccountsSchemaTest.php Verifies accounts table constraints and index.
tests/AccountsMigrationTest.php Verifies legacy token migration behavior.
tests/AccountsCrudTest.php Exercises accounts CRUD + deleteDatabase preservation behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread workflow.php
Comment on lines +61 to +65
self::ensureAccountsTable();
self::migrateRequestCacheSchema();

if (!self::$enterprise) {
self::migrateLegacyAccessToken();
Comment thread workflow.php
PRIMARY KEY (account_id, url)
) WITHOUT ROWID
');
self::$db->exec('CREATE INDEX parent_url ON request_cache(parent) WHERE parent IS NOT NULL');
Comment thread workflow.php
$stmt = self::$db->prepare(
'INSERT INTO accounts (label, token, is_active, created_at) VALUES (?, ?, 1, ?)'
);
$stmt->execute(['default', $legacyToken, time()]);
Comment thread action.php
Comment on lines +119 to +128
private static function showAlert(string $title, string $message): void
{
$safeTitle = str_replace('"', '\\"', $title);
$safeMessage = str_replace('"', '\\"', $message);
$script = 'set r to display dialog "'.$safeMessage.'" buttons {"OK", "Open GitHub"} default button "Open GitHub" with title "'.$safeTitle.'"'."\n".
'if button returned of r is "Open GitHub" then'."\n".
' open location "https://github.com"'."\n".
'end if';
exec('osascript -e '.escapeshellarg($script).' > /dev/null 2>&1 &');
}
Comment thread action.php
Comment on lines +137 to +142
if ($enterprise) {
return 'Multi-account is only supported for github.com.';
}
if ('' === $label) {
return 'Usage: gh user add <label>';
}
Comment thread server.php
Comment on lines +30 to +31
unset($ch);
if (200 === $status) {
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.

2 participants