Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
90a7122
feat(github): add safe connection management foundation
pandemicsyn Sep 5, 2026
468364a
fix(github): close connection lifecycle races
pandemicsyn Sep 8, 2026
dc43f7e
fix(github): complete canonical shadow coverage
pandemicsyn Sep 8, 2026
c31459d
fix(github): pin remaining connection consumers
pandemicsyn Sep 8, 2026
6914855
fix(github): pin final tenant routing paths
pandemicsyn Sep 8, 2026
04360dd
fix(github): preserve app identity in link tokens
pandemicsyn Sep 8, 2026
02b10cc
fix(github): scope alternate link OAuth by app
pandemicsyn Sep 8, 2026
0cecc36
fix(db): backfill canonical GitHub installations on deploy
pandemicsyn Sep 8, 2026
4858618
fix(github): harden canonical reconciliation
pandemicsyn Sep 8, 2026
bb6f5b4
fix(github): align reconciliation eligibility
pandemicsyn Sep 8, 2026
ebfec9a
test(db): execute GitHub installation backfill
pandemicsyn Sep 8, 2026
13ab0e7
test(db): assert excluded GitHub canonicals
pandemicsyn Sep 8, 2026
735bae8
fix(db): regenerate GitHub connection migration
pandemicsyn Sep 8, 2026
608ce7f
test(db): target regenerated GitHub migration
pandemicsyn Sep 8, 2026
c40eb86
fix(github): serialize legacy installation writers
pandemicsyn Sep 8, 2026
e66e50e
test(github): cover concurrent organization writers
pandemicsyn Sep 8, 2026
3d2ef9a
fix(github): serialize organization writer cardinality
pandemicsyn Sep 8, 2026
eed5c2c
fix(github): scope owner cardinality locking
pandemicsyn Sep 8, 2026
339dfad
test(github): observe owner advisory lock contention
pandemicsyn Sep 8, 2026
e4311da
test(github): scope advisory lock observation
pandemicsyn Sep 8, 2026
78a1d9e
test(github): settle individual lock operations
pandemicsyn Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ Manage shared web env var additions and rotations with `pnpm web:env set <VARIAB
- `GITHUB_LITE_APP_PRIVATE_KEY` - Private key for the lite GitHub App. `[SECRET]`
- `GITHUB_LITE_APP_CLIENT_ID` - OAuth Client ID for the lite GitHub App install/login flow. [PUBLIC]
- `GITHUB_MULTIPLE_INSTALLATION_ORGANIZATION_IDS` - Comma-separated Kilo organization UUIDs allowed to connect multiple GitHub App installations. Unset or empty disables multiple installations for all organizations. [SERVER]
- `GITHUB_CONNECTION_MANAGEMENT_ENABLED` - Set to exact `true` to admit new existing-installation connection management and local disconnect. Unset or any other value keeps new management admission disabled without changing incumbent GitHub integration workflows. [SERVER]
- `GITHUB_ADMIN_STATS_TOKEN` - Token for admin GitHub API stats lookups; used in `apps/web/src/scripts/backfill-pr-author-github-ids.ts`. `[SECRET]`
- `GITHUB_CLI_PAT` - GitHub personal access token for `gh` CLI operations inside contractors; used in `services/gastown/container/src/process-manager.ts`. `[SECRET]`
- `GITHUB_TOKEN` - Generic GitHub token for API calls used as fallback when `GIT_TOKEN` or `GITHUB_CLI_PAT` is absent; used in `services/gastown/container/src/process-manager.ts`. `[SECRET]`
Expand Down
139 changes: 130 additions & 9 deletions apps/web/src/app/api/integrations/github/callback/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import { linkKiloUser } from '@/lib/bot-identity';
import { bot } from '@/lib/bot';
import { failureResult } from '@/lib/maybe-result';
import { consumeInstallState } from '@/lib/integrations/github/install-state';
import {
bindGitHubIntegrationToCanonicalInstallation,
observeGitHubInstallationLifecycle,
} from '@/lib/integrations/db/github-installations';
import type * as InstallStateModule from '@/lib/integrations/github/install-state';
import { db } from '@/lib/drizzle';
import {
Expand All @@ -18,15 +22,18 @@ import {
} from '@kilocode/db/schema';
import { eq } from 'drizzle-orm';
import { randomUUID } from 'node:crypto';
import type { Owner } from '@/lib/integrations/core/types';
import {
findIntegrationByInstallationId,
findIntegrationByInstallationIdForOwner,
upsertPlatformIntegrationForOwner,
} from '@/lib/integrations/db/platform-integrations';
import { isOrganizationMember } from '@/lib/organizations/organizations';
import { assertUserAdministersInstallation } from '@/lib/integrations/platforms/github/app-selector';
import { verifyGitHubInstallationAuthorization } from '@/lib/integrations/github/installation-authorization';
import { captureException, captureMessage } from '@sentry/nextjs';
import type { StateAdapter } from 'chat';
import { ensureOrganizationAccess } from '@/routers/organizations/utils';
import { assertUserAdministersInstallation } from '@/lib/integrations/platforms/github/app-selector';

const mockState = { kind: 'state' } as unknown as StateAdapter;

Expand Down Expand Up @@ -62,15 +69,33 @@ jest.mock('@/lib/integrations/platforms/github/app-selector', () => ({
})),
assertUserAdministersInstallation: jest.fn(async () => true),
}));
jest.mock('@/lib/integrations/github/installation-authorization', () => ({
verifyGitHubInstallationAuthorization: jest.fn(async () => ({
identity: { id: GITHUB_USER_ID, login: 'octocat' },
candidate: {
installationId: INSTALLATION_ID,
accountId: '1',
accountLogin: 'octocat',
accountType: 'Organization',
},
})),
}));
jest.mock('@/routers/organizations/utils', () => ({
ensureOrganizationAccess: jest.fn(),
}));
jest.mock('@/lib/integrations/db/platform-integrations', () => ({
createPendingIntegration: jest.fn(),
findIntegrationByInstallationId: jest.fn(),
findIntegrationByInstallationIdForOwner: jest.fn(),
findPendingInstallationByRequesterId: jest.fn(),
upsertPlatformIntegrationForOwner: jest.fn(async () => ({ ok: true })),
}));
jest.mock('@/lib/integrations/db/github-installations', () => ({
connectVerifiedGitHubInstallation: jest.fn(),
observeGitHubInstallationLifecycle: jest.fn(),
bindGitHubIntegrationToCanonicalInstallation: jest.fn(),
updateGitHubInstallationRepositories: jest.fn(),
}));
jest.mock('@/lib/organizations/organizations', () => ({
isOrganizationMember: jest.fn(),
}));
Expand All @@ -88,15 +113,25 @@ const mockedExchangeGitHubOAuthCode = jest.mocked(exchangeGitHubOAuthCode);
const mockedLinkKiloUser = jest.mocked(linkKiloUser);
const mockedBot = jest.mocked(bot);
const mockedFindIntegrationByInstallationId = jest.mocked(findIntegrationByInstallationId);
const mockedFindIntegrationByInstallationIdForOwner = jest.mocked(
findIntegrationByInstallationIdForOwner
);
const mockedCreateAppAuth = jest.mocked(createAppAuth);
const mockedOctokit = jest.mocked(Octokit);
const mockedUpsertPlatformIntegrationForOwner = jest.mocked(upsertPlatformIntegrationForOwner);
const mockedIsOrganizationMember = jest.mocked(isOrganizationMember);
const mockedConsumeInstallState = jest.mocked(consumeInstallState);
const mockedAssertUserAdministersInstallation = jest.mocked(assertUserAdministersInstallation);
const mockedVerifyGitHubInstallationAuthorization = jest.mocked(
verifyGitHubInstallationAuthorization
);
const mockedCaptureException = jest.mocked(captureException);
const mockedCaptureMessage = jest.mocked(captureMessage);
const mockedEnsureOrganizationAccess = jest.mocked(ensureOrganizationAccess);
const mockedAssertUserAdministersInstallation = jest.mocked(assertUserAdministersInstallation);
const mockedObserveGitHubInstallationLifecycle = jest.mocked(observeGitHubInstallationLifecycle);
const mockedBindGitHubIntegrationToCanonicalInstallation = jest.mocked(
bindGitHubIntegrationToCanonicalInstallation
);

function mockConsumedInstallState(state: GitHubInstallState) {
mockedConsumeInstallState.mockResolvedValue({ status: 'success', state });
Expand All @@ -109,14 +144,42 @@ const INSTALLATION_ID = '98765';
const INSTALL_STATE_TOKEN = 'valid-database-token-for-callback-tests';

beforeEach(() => {
let writtenOwner: Owner = { type: 'user', id: USER_ID };
mockedConsumeInstallState.mockReset();
mockedConsumeInstallState.mockResolvedValue({ status: 'unusable', reason: 'not_found' });
mockedUpsertPlatformIntegrationForOwner.mockResolvedValue({ ok: true });
mockedUpsertPlatformIntegrationForOwner.mockImplementation(async owner => {
writtenOwner = owner;
return { ok: true };
});
mockedFindIntegrationByInstallationId.mockImplementation(
async () =>
({
id: '00000000-0000-4000-8000-000000000099',
owned_by_user_id: writtenOwner.type === 'user' ? writtenOwner.id : null,
owned_by_organization_id: writtenOwner.type === 'org' ? writtenOwner.id : null,
github_app_type: 'standard',
}) as never
);
mockedFindIntegrationByInstallationIdForOwner.mockImplementation(
async () =>
({
id: '00000000-0000-4000-8000-000000000099',
}) as never
);
mockedExchangeGitHubOAuthCode.mockResolvedValue({
id: GITHUB_USER_ID,
login: 'octocat',
accessToken: 'ghu_test-token',
});
mockedVerifyGitHubInstallationAuthorization.mockResolvedValue({
identity: { id: GITHUB_USER_ID, login: 'octocat' },
candidate: {
installationId: INSTALLATION_ID,
accountId: '1',
accountLogin: 'octocat',
accountType: 'Organization',
},
});
mockedAssertUserAdministersInstallation.mockResolvedValue(true);
});

Expand Down Expand Up @@ -218,7 +281,11 @@ describe('GET /api/integrations/github/callback bot link flow', () => {
await expect(response.text()).resolves.toContain(
'not a member of the organization that owns this GitHub integration'
);
expect(mockedFindIntegrationByInstallationId).toHaveBeenCalledWith('github', INSTALLATION_ID);
expect(mockedFindIntegrationByInstallationId).toHaveBeenCalledWith(
'github',
INSTALLATION_ID,
'standard'
);
expect(mockedExchangeGitHubOAuthCode).not.toHaveBeenCalled();
expect(mockedLinkKiloUser).not.toHaveBeenCalled();
});
Expand All @@ -232,12 +299,50 @@ describe('GET /api/integrations/github/callback bot link flow', () => {
expect(response.status).toBe(200);
await expect(response.text()).resolves.toContain('GitHub account octocat has been linked');
expect(mockedExchangeGitHubOAuthCode).toHaveBeenCalledWith('abc', 'standard');
expect(mockedFindIntegrationByInstallationId).toHaveBeenCalledWith('github', INSTALLATION_ID);
expect(mockedFindIntegrationByInstallationId).toHaveBeenCalledWith(
'github',
INSTALLATION_ID,
'standard'
);
expect(mockedIsOrganizationMember).toHaveBeenCalledWith('org_1', USER_ID);
expect(mockedBot.initialize).toHaveBeenCalled();
expect(mockedLinkKiloUser).toHaveBeenCalledWith(
mockState,
{ platform: 'github', teamId: INSTALLATION_ID, userId: GITHUB_USER_ID },
{
platform: 'github',
teamId: INSTALLATION_ID,
userId: GITHUB_USER_ID,
githubAppType: 'standard',
},
USER_ID
);
});

test('routes a Lite bot-link callback through the Lite app identity', async () => {
mockedVerifyGitHubBotLinkState.mockReturnValue({
userId: USER_ID,
installationId: INSTALLATION_ID,
callbackPath: '/github/link',
githubAppType: 'lite',
});
mockedFindIntegrationByInstallationId.mockResolvedValue({
owned_by_organization_id: 'org_1',
github_app_type: 'lite',
} as never);
const { GET } = await import('./route');
const response = await GET(
makeRequest('/api/integrations/github/callback?code=abc&state=signed') as never
);
expect(response.status).toBe(200);
expect(mockedFindIntegrationByInstallationId).toHaveBeenCalledWith(
'github',
INSTALLATION_ID,
'lite'
);
expect(mockedExchangeGitHubOAuthCode).toHaveBeenCalledWith('abc', 'lite');
expect(mockedLinkKiloUser).toHaveBeenCalledWith(
mockState,
expect.objectContaining({ githubAppType: 'lite' }),
USER_ID
);
});
Expand Down Expand Up @@ -453,7 +558,7 @@ describe('GET /api/integrations/github/callback database-backed install flow', (
expect(mockedExchangeGitHubOAuthCode).not.toHaveBeenCalled();
expect(mockedCreateAppAuth).not.toHaveBeenCalled();
expect(mockedOctokit).not.toHaveBeenCalled();
expect(mockedAssertUserAdministersInstallation).not.toHaveBeenCalled();
expect(mockedVerifyGitHubInstallationAuthorization).not.toHaveBeenCalled();
expect(mockedVerifyGitHubBotLinkState).not.toHaveBeenCalled();
const { createPendingIntegration } =
await import('@/lib/integrations/db/platform-integrations');
Expand Down Expand Up @@ -948,6 +1053,14 @@ describe('GET /api/integrations/github/callback database-backed install flow', (
expect(response.status).toBe(307);
// No organizationId for user-scoped install.
expectRedirectLocation(response, '/github-app?fromApp=1&github_install=success');
expect(mockedObserveGitHubInstallationLifecycle).toHaveBeenCalledWith(
expect.objectContaining({ installationId: INSTALLATION_ID, state: 'active' })
);
expect(mockedBindGitHubIntegrationToCanonicalInstallation).toHaveBeenCalledWith({
integrationId: '00000000-0000-4000-8000-000000000099',
installationId: INSTALLATION_ID,
appType: 'standard',
});
});

test('app-initiated org pending approval preserves organizationId', async () => {
Expand Down Expand Up @@ -1068,7 +1181,15 @@ describe('GET /api/integrations/github/callback admin proof', () => {
}) as never
);
mockedUpsertPlatformIntegrationForOwner.mockResolvedValue({ ok: true });
mockedAssertUserAdministersInstallation.mockResolvedValue(true);
mockedVerifyGitHubInstallationAuthorization.mockResolvedValue({
identity: { id: GITHUB_USER_ID, login: 'octocat' },
candidate: {
installationId: INSTALLATION_ID,
accountId: '1',
accountLogin: 'octocat',
accountType: 'Organization',
},
});
mockedExchangeGitHubOAuthCode.mockResolvedValue({
id: GITHUB_USER_ID,
login: 'octocat',
Expand All @@ -1088,7 +1209,7 @@ describe('GET /api/integrations/github/callback admin proof', () => {
expectRedirectLocation(response, `/integrations/github?error=not_installation_admin`);
expect(mockedUpsertPlatformIntegrationForOwner).not.toHaveBeenCalled();
expect(mockedExchangeGitHubOAuthCode).not.toHaveBeenCalled();
expect(mockedAssertUserAdministersInstallation).not.toHaveBeenCalled();
expect(mockedVerifyGitHubInstallationAuthorization).not.toHaveBeenCalled();
expect(mockedCreateAppAuth).not.toHaveBeenCalled();
});

Expand Down
Loading
Loading