refactor(web/server): consolidate GitHub utilities into single module (#436)
This commit is contained in:
@@ -41,6 +41,10 @@ Quota provider registry, dispatch, and provider integrations for usage endpoints
|
|||||||
Git repository operations for the web server runtime.
|
Git repository operations for the web server runtime.
|
||||||
- Module docs: `packages/web/server/lib/git/DOCUMENTATION.md`
|
- Module docs: `packages/web/server/lib/git/DOCUMENTATION.md`
|
||||||
|
|
||||||
|
##### github
|
||||||
|
GitHub authentication, OAuth device flow, Octokit client factory, and repository URL parsing.
|
||||||
|
- Module docs: `packages/web/server/lib/github/DOCUMENTATION.md`
|
||||||
|
|
||||||
## Build / dev commands (verified)
|
## Build / dev commands (verified)
|
||||||
All scripts are in `package.json`.
|
All scripts are in `package.json`.
|
||||||
- Validate: `bun run type-check`, `bun run lint`
|
- Validate: `bun run type-check`, `bun run lint`
|
||||||
|
|||||||
@@ -7216,17 +7216,12 @@ async function main(options = {}) {
|
|||||||
|
|
||||||
// ================= GitHub OAuth (Device Flow) =================
|
// ================= GitHub OAuth (Device Flow) =================
|
||||||
|
|
||||||
// Note: scopes may be overridden via OPENCHAMBER_GITHUB_SCOPES or settings.json (see github-auth.js).
|
// Note: scopes may be overridden via OPENCHAMBER_GITHUB_SCOPES or settings.json (see lib/github/auth.js).
|
||||||
|
|
||||||
let githubLibraries = null;
|
let githubLibraries = null;
|
||||||
const getGitHubLibraries = async () => {
|
const getGitHubLibraries = async () => {
|
||||||
if (!githubLibraries) {
|
if (!githubLibraries) {
|
||||||
const [auth, device, octokit] = await Promise.all([
|
githubLibraries = await import('./lib/github/index.js');
|
||||||
import('./lib/github-auth.js'),
|
|
||||||
import('./lib/github-device-flow.js'),
|
|
||||||
import('./lib/github-octokit.js'),
|
|
||||||
]);
|
|
||||||
githubLibraries = { ...auth, ...device, ...octokit };
|
|
||||||
}
|
}
|
||||||
return githubLibraries;
|
return githubLibraries;
|
||||||
};
|
};
|
||||||
@@ -7480,7 +7475,7 @@ async function main(options = {}) {
|
|||||||
return res.json({ connected: false });
|
return res.json({ connected: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js');
|
const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js');
|
||||||
const { repo } = await resolveGitHubRepoFromDirectory(directory, remote);
|
const { repo } = await resolveGitHubRepoFromDirectory(directory, remote);
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
return res.json({ connected: true, repo: null, branch, pr: null, checks: null, canMerge: false });
|
return res.json({ connected: true, repo: null, branch, pr: null, checks: null, canMerge: false });
|
||||||
@@ -7706,7 +7701,7 @@ async function main(options = {}) {
|
|||||||
return res.status(401).json({ error: 'GitHub not connected' });
|
return res.status(401).json({ error: 'GitHub not connected' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js');
|
const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js');
|
||||||
const { repo } = await resolveGitHubRepoFromDirectory(directory, remote);
|
const { repo } = await resolveGitHubRepoFromDirectory(directory, remote);
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
return res.status(400).json({ error: 'Unable to resolve GitHub repo from git remote' });
|
return res.status(400).json({ error: 'Unable to resolve GitHub repo from git remote' });
|
||||||
@@ -7883,7 +7878,7 @@ async function main(options = {}) {
|
|||||||
return res.status(401).json({ error: 'GitHub not connected' });
|
return res.status(401).json({ error: 'GitHub not connected' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js');
|
const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js');
|
||||||
const { repo } = await resolveGitHubRepoFromDirectory(directory);
|
const { repo } = await resolveGitHubRepoFromDirectory(directory);
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
return res.status(400).json({ error: 'Unable to resolve GitHub repo from git remote' });
|
return res.status(400).json({ error: 'Unable to resolve GitHub repo from git remote' });
|
||||||
@@ -7958,7 +7953,7 @@ async function main(options = {}) {
|
|||||||
return res.status(401).json({ error: 'GitHub not connected' });
|
return res.status(401).json({ error: 'GitHub not connected' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js');
|
const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js');
|
||||||
const { repo } = await resolveGitHubRepoFromDirectory(directory);
|
const { repo } = await resolveGitHubRepoFromDirectory(directory);
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
return res.status(400).json({ error: 'Unable to resolve GitHub repo from git remote' });
|
return res.status(400).json({ error: 'Unable to resolve GitHub repo from git remote' });
|
||||||
@@ -8001,7 +7996,7 @@ async function main(options = {}) {
|
|||||||
return res.status(401).json({ error: 'GitHub not connected' });
|
return res.status(401).json({ error: 'GitHub not connected' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js');
|
const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js');
|
||||||
const { repo } = await resolveGitHubRepoFromDirectory(directory);
|
const { repo } = await resolveGitHubRepoFromDirectory(directory);
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
return res.status(400).json({ error: 'Unable to resolve GitHub repo from git remote' });
|
return res.status(400).json({ error: 'Unable to resolve GitHub repo from git remote' });
|
||||||
@@ -8052,7 +8047,7 @@ async function main(options = {}) {
|
|||||||
return res.json({ connected: false });
|
return res.json({ connected: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js');
|
const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js');
|
||||||
const { repo } = await resolveGitHubRepoFromDirectory(directory);
|
const { repo } = await resolveGitHubRepoFromDirectory(directory);
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
return res.json({ connected: true, repo: null, issues: [] });
|
return res.json({ connected: true, repo: null, issues: [] });
|
||||||
@@ -8108,7 +8103,7 @@ async function main(options = {}) {
|
|||||||
return res.json({ connected: false });
|
return res.json({ connected: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js');
|
const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js');
|
||||||
const { repo } = await resolveGitHubRepoFromDirectory(directory);
|
const { repo } = await resolveGitHubRepoFromDirectory(directory);
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
return res.json({ connected: true, repo: null, issue: null });
|
return res.json({ connected: true, repo: null, issue: null });
|
||||||
@@ -8169,7 +8164,7 @@ async function main(options = {}) {
|
|||||||
return res.json({ connected: false });
|
return res.json({ connected: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js');
|
const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js');
|
||||||
const { repo } = await resolveGitHubRepoFromDirectory(directory);
|
const { repo } = await resolveGitHubRepoFromDirectory(directory);
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
return res.json({ connected: true, repo: null, comments: [] });
|
return res.json({ connected: true, repo: null, comments: [] });
|
||||||
@@ -8214,7 +8209,7 @@ async function main(options = {}) {
|
|||||||
return res.json({ connected: false });
|
return res.json({ connected: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js');
|
const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js');
|
||||||
const { repo } = await resolveGitHubRepoFromDirectory(directory);
|
const { repo } = await resolveGitHubRepoFromDirectory(directory);
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
return res.json({ connected: true, repo: null, prs: [] });
|
return res.json({ connected: true, repo: null, prs: [] });
|
||||||
@@ -8289,7 +8284,7 @@ async function main(options = {}) {
|
|||||||
return res.json({ connected: false });
|
return res.json({ connected: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js');
|
const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js');
|
||||||
const { repo } = await resolveGitHubRepoFromDirectory(directory);
|
const { repo } = await resolveGitHubRepoFromDirectory(directory);
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
return res.json({ connected: true, repo: null, pr: null });
|
return res.json({ connected: true, repo: null, pr: null });
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# GitHub Module Documentation
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
This module provides GitHub authentication, OAuth device flow, Octokit client factory, and repository URL parsing utilities for the web server runtime.
|
||||||
|
|
||||||
|
## Entrypoints and structure
|
||||||
|
- `packages/web/server/lib/github/index.js`: public entrypoint imported by `packages/web/server/index.js`.
|
||||||
|
- `packages/web/server/lib/github/auth.js`: auth storage, multi-account support, and client ID/scope configuration.
|
||||||
|
- `packages/web/server/lib/github/device-flow.js`: OAuth device code flow implementation for browserless auth.
|
||||||
|
- `packages/web/server/lib/github/octokit.js`: Octokit client factory backed by current auth.
|
||||||
|
- `packages/web/server/lib/github/repo/index.js`: GitHub remote URL parser and directory-to-repo resolver.
|
||||||
|
|
||||||
|
## Public exports (from index.js)
|
||||||
|
|
||||||
|
### Auth (`auth.js`)
|
||||||
|
- `getGitHubAuth()`: Returns current auth entry (accessToken, user, scope, accountId).
|
||||||
|
- `getGitHubAuthAccounts()`: Returns list of all configured accounts.
|
||||||
|
- `setGitHubAuth({ accessToken, scope, tokenType, user, accountId })`: Stores or updates auth entry.
|
||||||
|
- `activateGitHubAuth(accountId)`: Sets specified account as current.
|
||||||
|
- `clearGitHubAuth()`: Removes current account or deletes storage file if last account.
|
||||||
|
- `getGitHubClientId()`: Resolves client ID from env var, settings.json, or default.
|
||||||
|
- `getGitHubScopes()`: Resolves scopes from env var, settings.json, or default.
|
||||||
|
- `GITHUB_AUTH_FILE`: Storage file path constant.
|
||||||
|
|
||||||
|
### Device flow (`device-flow.js`)
|
||||||
|
- `startDeviceFlow({ clientId, scope })`: Requests device code from GitHub.
|
||||||
|
- `exchangeDeviceCode({ clientId, deviceCode })`: Polls for access token.
|
||||||
|
|
||||||
|
### Octokit (`octokit.js`)
|
||||||
|
- `getOctokitOrNull()`: Returns configured Octokit instance or null if no auth.
|
||||||
|
|
||||||
|
### Repo (`repo/index.js`)
|
||||||
|
- `parseGitHubRemoteUrl(raw)`: Parses SSH/HTTPS URLs into `{ owner, repo, url }`.
|
||||||
|
- `resolveGitHubRepoFromDirectory(directory, remoteName)`: Resolves GitHub repo from git remote.
|
||||||
|
|
||||||
|
## Storage and configuration
|
||||||
|
- Auth storage: `~/.config/openchamber/github-auth.json` (atomic writes, mode 0o600).
|
||||||
|
- Client ID: `OPENCHAMBER_GITHUB_CLIENT_ID` env var → `settings.json` → default.
|
||||||
|
- Scopes: `OPENCHAMBER_GITHUB_SCOPES` env var → `settings.json` → default.
|
||||||
|
|
||||||
|
## Account resolution
|
||||||
|
Account IDs are resolved in priority order: explicit `accountId` → user login → user ID → token prefix.
|
||||||
|
|
||||||
|
## Notes for contributors
|
||||||
|
- All auth operations use atomic file writes for safe multi-instance sharing.
|
||||||
|
- Device flow handles GitHub's `authorization_pending` responses at caller level.
|
||||||
|
- Repo parser supports `git@github.com:`, `ssh://git@github.com/`, and `https://github.com/` URL formats.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export {
|
||||||
|
getGitHubAuth,
|
||||||
|
getGitHubAuthAccounts,
|
||||||
|
setGitHubAuth,
|
||||||
|
activateGitHubAuth,
|
||||||
|
clearGitHubAuth,
|
||||||
|
getGitHubClientId,
|
||||||
|
getGitHubScopes,
|
||||||
|
GITHUB_AUTH_FILE,
|
||||||
|
} from './auth.js';
|
||||||
|
|
||||||
|
export {
|
||||||
|
startDeviceFlow,
|
||||||
|
exchangeDeviceCode,
|
||||||
|
} from './device-flow.js';
|
||||||
|
|
||||||
|
export {
|
||||||
|
getOctokitOrNull,
|
||||||
|
} from './octokit.js';
|
||||||
|
|
||||||
|
export {
|
||||||
|
parseGitHubRemoteUrl,
|
||||||
|
resolveGitHubRepoFromDirectory,
|
||||||
|
} from './repo/index.js';
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { Octokit } from '@octokit/rest';
|
import { Octokit } from '@octokit/rest';
|
||||||
import { getGitHubAuth } from './github-auth.js';
|
import { getGitHubAuth } from './auth.js';
|
||||||
|
|
||||||
export function getOctokitOrNull() {
|
export function getOctokitOrNull() {
|
||||||
const auth = getGitHubAuth();
|
const auth = getGitHubAuth();
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { getRemoteUrl } from './git/index.js';
|
import { getRemoteUrl } from '../../git/index.js';
|
||||||
|
|
||||||
export const parseGitHubRemoteUrl = (raw) => {
|
export const parseGitHubRemoteUrl = (raw) => {
|
||||||
if (typeof raw !== 'string') {
|
if (typeof raw !== 'string') {
|
||||||
Reference in New Issue
Block a user