mirror of https://github.com/buster-so/buster.git
update to ci, fix greptile recs
This commit is contained in:
parent
38303771f7
commit
db63ac4c83
|
@ -160,25 +160,4 @@ jobs:
|
|||
path: |
|
||||
**/coverage/**
|
||||
!**/coverage/tmp/**
|
||||
retention-days: 7
|
||||
|
||||
# Final status check - ensures all parallel jobs pass
|
||||
ci-status:
|
||||
name: CI Status
|
||||
needs: [build, lint, test]
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
if: always()
|
||||
timeout-minutes: 1
|
||||
steps:
|
||||
- name: Check CI Status
|
||||
run: |
|
||||
if [[ "${{ needs.build.result }}" != "success" ||
|
||||
"${{ needs.lint.result }}" != "success" ||
|
||||
"${{ needs.test.result }}" != "success" ]]; then
|
||||
echo "❌ CI failed"
|
||||
echo "Build: ${{ needs.build.result }}"
|
||||
echo "Lint: ${{ needs.lint.result }}"
|
||||
echo "Test: ${{ needs.test.result }}"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ All CI checks passed!"
|
||||
retention-days: 7
|
|
@ -80,7 +80,9 @@ export async function authCallbackHandler(
|
|||
const callbackPayload = {
|
||||
action: 'created' as const,
|
||||
installation: {
|
||||
id: Number.parseInt(request.installation_id, 10),
|
||||
id: Number.isNaN(Number(request.installation_id))
|
||||
? 0
|
||||
: Number.parseInt(request.installation_id, 10),
|
||||
account: {
|
||||
// These will be updated when the webhook arrives with full details
|
||||
id: 0,
|
||||
|
|
|
@ -277,12 +277,12 @@ describe('Auth Init Handler Integration Tests', () => {
|
|||
const metadata = JSON.parse(storedState.description);
|
||||
expect(metadata.expiresAt).toBeTruthy();
|
||||
|
||||
// Should expire in 15 minutes
|
||||
// Should expire in 10 minutes
|
||||
const expiresAt = new Date(metadata.expiresAt);
|
||||
const now = new Date();
|
||||
const diffMinutes = (expiresAt.getTime() - now.getTime()) / (1000 * 60);
|
||||
expect(diffMinutes).toBeGreaterThan(14);
|
||||
expect(diffMinutes).toBeLessThan(16);
|
||||
expect(diffMinutes).toBeGreaterThan(9);
|
||||
expect(diffMinutes).toBeLessThan(11);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -99,7 +99,7 @@ describe('webhookHandler', () => {
|
|||
const result = await webhookHandler(suspendedPayload);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('Installation suspend successfully');
|
||||
expect(result.message).toBe('Installation suspended successfully');
|
||||
});
|
||||
|
||||
it('should handle installation unsuspended', async () => {
|
||||
|
@ -116,7 +116,7 @@ describe('webhookHandler', () => {
|
|||
const result = await webhookHandler(unsuspendedPayload);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('Installation unsuspend successfully');
|
||||
expect(result.message).toBe('Installation unsuspended successfully');
|
||||
});
|
||||
|
||||
it('should throw 400 for created action without org context', async () => {
|
||||
|
|
|
@ -26,10 +26,17 @@ export async function webhookHandler(
|
|||
userId: userId || '',
|
||||
});
|
||||
|
||||
const actionMessages: Record<string, string> = {
|
||||
deleted: 'Installation deleted successfully',
|
||||
suspend: 'Installation suspended successfully',
|
||||
unsuspend: 'Installation unsuspended successfully',
|
||||
new_permissions_accepted: 'Installation permissions updated successfully',
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
integration: result,
|
||||
message: `Installation ${payload.action} successfully`,
|
||||
message: actionMessages[payload.action] || `Installation ${payload.action} successfully`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to handle installation callback:', error);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { GitHubErrorCode } from '@buster/server-shared/github';
|
||||
import { App } from 'octokit';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createGitHubApp, getGitHubAppCredentials } from './github-app';
|
||||
|
@ -86,7 +85,7 @@ describe('github-app', () => {
|
|||
|
||||
// Act & Assert
|
||||
expect(() => getGitHubAppCredentials()).toThrow(
|
||||
'Failed to decode GITHUB_APP_PRIVATE_KEY_BASE64'
|
||||
'Failed to decode GITHUB_APP_PRIVATE_KEY_BASE64: Invalid base64 encoding'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -98,7 +97,9 @@ describe('github-app', () => {
|
|||
process.env.GITHUB_WEBHOOK_SECRET = 'test';
|
||||
|
||||
// Act & Assert
|
||||
expect(() => getGitHubAppCredentials()).toThrow('Invalid GitHub App private key format');
|
||||
expect(() => getGitHubAppCredentials()).toThrow(
|
||||
'Invalid GitHub App private key format. Expected PEM-encoded RSA private key or PKCS#8 private key'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -38,11 +38,12 @@ export function getGitHubAppCredentials(): {
|
|||
// Decode the private key from base64
|
||||
let privateKey: string;
|
||||
try {
|
||||
// Check if it's valid base64 first
|
||||
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(privateKeyBase64)) {
|
||||
// Check if it's valid base64 first (allow whitespace which will be trimmed)
|
||||
const trimmedBase64 = privateKeyBase64.trim();
|
||||
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(trimmedBase64)) {
|
||||
throw new Error('Invalid base64 format');
|
||||
}
|
||||
privateKey = Buffer.from(privateKeyBase64, 'base64').toString('utf-8');
|
||||
privateKey = Buffer.from(trimmedBase64, 'base64').toString('utf-8');
|
||||
} catch (_error) {
|
||||
throw createGitHubError(
|
||||
GitHubErrorCode.APP_CONFIGURATION_ERROR,
|
||||
|
@ -50,11 +51,11 @@ export function getGitHubAppCredentials(): {
|
|||
);
|
||||
}
|
||||
|
||||
// Validate the private key format
|
||||
if (!privateKey.includes('BEGIN RSA PRIVATE KEY')) {
|
||||
// Validate the private key format (support both RSA and PKCS#8 formats)
|
||||
if (!privateKey.includes('BEGIN RSA PRIVATE KEY') && !privateKey.includes('BEGIN PRIVATE KEY')) {
|
||||
throw createGitHubError(
|
||||
GitHubErrorCode.APP_CONFIGURATION_ERROR,
|
||||
'Invalid GitHub App private key format. Expected PEM-encoded RSA private key'
|
||||
'Invalid GitHub App private key format. Expected PEM-encoded RSA private key or PKCS#8 private key'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -99,9 +99,14 @@ async function handleInstallationCreated(params: {
|
|||
}
|
||||
|
||||
// Generate and store new token
|
||||
await generateAndStoreToken(installation.id.toString());
|
||||
const tokenVaultKey = await generateAndStoreToken(installation.id.toString());
|
||||
|
||||
return updated;
|
||||
// Update the integration with the new vault key
|
||||
const fullyUpdated = await updateGithubIntegration(existing.id, {
|
||||
tokenVaultKey,
|
||||
});
|
||||
|
||||
return fullyUpdated || updated;
|
||||
}
|
||||
|
||||
// Create new integration
|
||||
|
|
|
@ -22,6 +22,8 @@ export async function storeInstallationState(
|
|||
data: InstallationState
|
||||
): Promise<void> {
|
||||
const key = generateStateVaultKey(state);
|
||||
const expirationTime = new Date(Date.now() + 10 * 60 * 1000).toISOString();
|
||||
const description = `GitHub OAuth state expires at ${expirationTime}`;
|
||||
|
||||
try {
|
||||
// Check if state already exists (shouldn't happen with random generation)
|
||||
|
@ -32,13 +34,13 @@ export async function storeInstallationState(
|
|||
id: existing.id,
|
||||
secret: JSON.stringify(data),
|
||||
name: key,
|
||||
description: `GitHub OAuth state expires at ${new Date(Date.now() + 10 * 60 * 1000).toISOString()}`,
|
||||
description,
|
||||
});
|
||||
} else {
|
||||
await createSecret({
|
||||
secret: JSON.stringify(data),
|
||||
name: key,
|
||||
description: `GitHub OAuth state expires at ${new Date(Date.now() + 10 * 60 * 1000).toISOString()}`,
|
||||
description,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -65,7 +67,14 @@ export async function retrieveInstallationState(state: string): Promise<Installa
|
|||
}
|
||||
|
||||
// Check if state is expired (10 minutes)
|
||||
const data = JSON.parse(secret.secret) as InstallationState;
|
||||
let data: InstallationState;
|
||||
try {
|
||||
data = JSON.parse(secret.secret) as InstallationState;
|
||||
} catch (error) {
|
||||
console.error('Failed to parse OAuth state data:', error);
|
||||
await deleteSecret(secret.id);
|
||||
return null;
|
||||
}
|
||||
const createdAt = new Date(data.createdAt);
|
||||
const now = new Date();
|
||||
const tenMinutes = 10 * 60 * 1000;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
import { deleteSecret, getSecretByName } from '@buster/database';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
deleteInstallationToken,
|
||||
generateTokenVaultKey,
|
||||
|
|
|
@ -91,13 +91,13 @@ export async function retrieveInstallationToken(
|
|||
? (JSON.parse(secret.description) as TokenMetadata)
|
||||
: {
|
||||
installationId,
|
||||
expiresAt: new Date().toISOString(), // Default to expired if no metadata
|
||||
expiresAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), // Default to expired (24 hours ago) if no metadata
|
||||
};
|
||||
} catch {
|
||||
// If description is not valid JSON, create minimal metadata
|
||||
metadata = {
|
||||
installationId,
|
||||
expiresAt: new Date().toISOString(),
|
||||
expiresAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), // Default to expired (24 hours ago)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -95,7 +95,7 @@ describe('getGithubIntegrationByInstallationId', () => {
|
|||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up test GitHub integrations
|
||||
// Clean up test GitHub integrations first (due to foreign key constraints)
|
||||
await db.delete(githubIntegrations).where(eq(githubIntegrations.organizationId, testOrgId));
|
||||
|
||||
// Clean up test user
|
||||
|
@ -103,7 +103,7 @@ describe('getGithubIntegrationByInstallationId', () => {
|
|||
await db.delete(users).where(eq(users.id, testUserId));
|
||||
}
|
||||
|
||||
// Clean up test organization
|
||||
// Clean up test organization last
|
||||
if (testOrgId) {
|
||||
await db.delete(organizations).where(eq(organizations.id, testOrgId));
|
||||
}
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
import { and, eq, isNull } from 'drizzle-orm';
|
||||
import type { InferSelectModel } from 'drizzle-orm';
|
||||
import { db } from '../../connection';
|
||||
import { githubIntegrations } from '../../schema';
|
||||
|
||||
type GitHubIntegration = InferSelectModel<typeof githubIntegrations>;
|
||||
|
||||
/**
|
||||
* Get GitHub integration by installation ID
|
||||
*/
|
||||
export async function getGithubIntegrationByInstallationId(installationId: string) {
|
||||
export async function getGithubIntegrationByInstallationId(
|
||||
installationId: string
|
||||
): Promise<GitHubIntegration | undefined> {
|
||||
const [integration] = await db
|
||||
.select()
|
||||
.from(githubIntegrations)
|
||||
|
|
|
@ -24,6 +24,7 @@ node_modules/
|
|||
.DS_Store
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
|
|
|
@ -35,5 +35,10 @@
|
|||
"@buster/server-shared": "workspace:*",
|
||||
"octokit": "^5.0.3",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,10 @@ loadRootEnv();
|
|||
// Note: These are only required at runtime when using the GitHub integration
|
||||
// Making them optional for build time to allow packages to be built without GitHub setup
|
||||
const requiredEnv = {
|
||||
// NODE_ENV is optional - will default to 'development' if not set
|
||||
// GitHub variables are optional at build time but required at runtime
|
||||
// GitHub App configuration (required for runtime)
|
||||
GITHUB_APP_ID: process.env.GITHUB_APP_ID,
|
||||
GITHUB_APP_PRIVATE_KEY_BASE64: process.env.GITHUB_APP_PRIVATE_KEY_BASE64,
|
||||
GITHUB_WEBHOOK_SECRET: process.env.GITHUB_WEBHOOK_SECRET,
|
||||
};
|
||||
|
||||
// Validate environment variables
|
||||
|
|
|
@ -38,11 +38,12 @@ export function getGitHubAppCredentials(): {
|
|||
// Decode the private key from base64
|
||||
let privateKey: string;
|
||||
try {
|
||||
// Check if it's valid base64 first
|
||||
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(privateKeyBase64)) {
|
||||
// Check if it's valid base64 first (allow whitespace which will be trimmed)
|
||||
const trimmedBase64 = privateKeyBase64.trim();
|
||||
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(trimmedBase64)) {
|
||||
throw new Error('Invalid base64 format');
|
||||
}
|
||||
privateKey = Buffer.from(privateKeyBase64, 'base64').toString('utf-8');
|
||||
privateKey = Buffer.from(trimmedBase64, 'base64').toString('utf-8');
|
||||
} catch (_error) {
|
||||
throw createGitHubError(
|
||||
GitHubErrorCode.APP_CONFIGURATION_ERROR,
|
||||
|
@ -50,11 +51,11 @@ export function getGitHubAppCredentials(): {
|
|||
);
|
||||
}
|
||||
|
||||
// Validate the private key format
|
||||
if (!privateKey.includes('BEGIN RSA PRIVATE KEY')) {
|
||||
// Validate the private key format (support both RSA and PKCS#8 formats)
|
||||
if (!privateKey.includes('BEGIN RSA PRIVATE KEY') && !privateKey.includes('BEGIN PRIVATE KEY')) {
|
||||
throw createGitHubError(
|
||||
GitHubErrorCode.APP_CONFIGURATION_ERROR,
|
||||
'Invalid GitHub App private key format. Expected PEM-encoded RSA private key'
|
||||
'Invalid GitHub App private key format. Expected PEM-encoded private key'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,2 @@
|
|||
// Export your library functions here
|
||||
export const howdy = () => {
|
||||
return 'Hello from @buster/github!';
|
||||
};
|
||||
// GitHub library utilities
|
||||
// Additional utility functions can be added here as needed
|
||||
|
|
|
@ -115,13 +115,13 @@ export async function retrieveInstallationToken(
|
|||
? (JSON.parse(secret.description) as TokenMetadata)
|
||||
: {
|
||||
installationId,
|
||||
expiresAt: new Date().toISOString(), // Default to expired if no metadata
|
||||
expiresAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), // Default to expired (24 hours ago) if no metadata
|
||||
};
|
||||
} catch {
|
||||
// If description is not valid JSON, create minimal metadata
|
||||
metadata = {
|
||||
installationId,
|
||||
expiresAt: new Date().toISOString(),
|
||||
expiresAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), // Default to expired (24 hours ago)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { createHmac } from 'node:crypto';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { skipIfNoGitHubCredentials } from '../../../../apps/server/src/api/v2/github/test-helpers/github-test-setup';
|
||||
import { verifyWebhookSignature } from './webhook';
|
||||
import { verifyGitHubWebhookSignature } from './webhook';
|
||||
|
||||
describe('GitHub Webhook Service Integration Tests', () => {
|
||||
describe('Webhook Signature Verification', () => {
|
||||
|
@ -30,7 +30,7 @@ describe('GitHub Webhook Service Integration Tests', () => {
|
|||
const signature = `sha256=${createHmac('sha256', webhookSecret).update(payloadString).digest('hex')}`;
|
||||
|
||||
// Should verify successfully
|
||||
const isValid = verifyWebhookSignature(payloadString, signature, webhookSecret);
|
||||
const isValid = verifyGitHubWebhookSignature(payloadString, signature);
|
||||
expect(isValid).toBe(true);
|
||||
});
|
||||
|
||||
|
@ -77,7 +77,7 @@ describe('GitHub Webhook Service Integration Tests', () => {
|
|||
];
|
||||
|
||||
for (const signature of wrongFormats) {
|
||||
const isValid = verifyWebhookSignature(payloadString, signature, webhookSecret);
|
||||
const isValid = verifyGitHubWebhookSignature(payloadString, signature);
|
||||
expect(isValid).toBe(false);
|
||||
}
|
||||
});
|
||||
|
@ -114,7 +114,7 @@ describe('GitHub Webhook Service Integration Tests', () => {
|
|||
const payloadString = JSON.stringify(payload);
|
||||
const signature = `sha256=${createHmac('sha256', webhookSecret).update(payloadString).digest('hex')}`;
|
||||
|
||||
const isValid = verifyWebhookSignature(payloadString, signature, webhookSecret);
|
||||
const isValid = verifyGitHubWebhookSignature(payloadString, signature);
|
||||
expect(isValid).toBe(true);
|
||||
}
|
||||
});
|
||||
|
@ -132,7 +132,7 @@ describe('GitHub Webhook Service Integration Tests', () => {
|
|||
|
||||
// Verify multiple times - should always return same result
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const isValid = verifyWebhookSignature(payloadString, signature, webhookSecret);
|
||||
const isValid = verifyGitHubWebhookSignature(payloadString, signature);
|
||||
expect(isValid).toBe(true);
|
||||
}
|
||||
});
|
||||
|
@ -241,7 +241,7 @@ describe('GitHub Webhook Service Integration Tests', () => {
|
|||
const payloadString = JSON.stringify(largePayload);
|
||||
const signature = `sha256=${createHmac('sha256', webhookSecret).update(payloadString).digest('hex')}`;
|
||||
|
||||
const isValid = verifyWebhookSignature(payloadString, signature, webhookSecret);
|
||||
const isValid = verifyGitHubWebhookSignature(payloadString, signature);
|
||||
expect(isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@ import { RuntimeContext } from '@mastra/core/runtime-context';
|
|||
|
||||
// Option 1: Create sandbox with GitHub token
|
||||
const runtimeContext = new RuntimeContext();
|
||||
const githubToken = await fetchGitHubToken(organizationId);
|
||||
const githubToken = await getInstallationTokenByOrgId(organizationId);
|
||||
const sandbox = await createSandboxWithGitHubToken(runtimeContext, githubToken);
|
||||
|
||||
// Option 2: Add token to existing context
|
||||
|
|
|
@ -3,7 +3,7 @@ import { z } from 'zod';
|
|||
// GitHub App Installation Webhook Payload
|
||||
// Received when a GitHub App is installed, deleted, suspended, or unsuspended
|
||||
export const InstallationCallbackSchema = z.object({
|
||||
action: z.enum(['created', 'deleted', 'suspend', 'unsuspend']),
|
||||
action: z.enum(['created', 'deleted', 'suspend', 'unsuspend', 'new_permissions_accepted']),
|
||||
installation: z.object({
|
||||
id: z.number(), // GitHub installation ID
|
||||
account: z.object({
|
||||
|
|
|
@ -22,7 +22,7 @@ export type InstallationCallbackResponse = z.infer<typeof InstallationCallbackRe
|
|||
// Response containing installation access token
|
||||
export const InstallationTokenResponseSchema = z.object({
|
||||
token: z.string(),
|
||||
expires_at: z.string(), // ISO 8601 date string
|
||||
expires_at: z.string().datetime(), // ISO 8601 date string
|
||||
permissions: z.record(z.string()).optional(), // e.g., { "contents": "read", "issues": "write" }
|
||||
repository_selection: z.enum(['all', 'selected']).optional(),
|
||||
repositories: z
|
||||
|
@ -49,8 +49,8 @@ export const GetGitHubIntegrationResponseSchema = z.object({
|
|||
github_org_name: z.string(),
|
||||
github_org_id: z.string(),
|
||||
installation_id: z.string(),
|
||||
installed_at: z.string(),
|
||||
last_used_at: z.string().optional(),
|
||||
installed_at: z.string().datetime(),
|
||||
last_used_at: z.string().datetime().optional(),
|
||||
repository_count: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
|
Loading…
Reference in New Issue