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: |
|
path: |
|
||||||
**/coverage/**
|
**/coverage/**
|
||||||
!**/coverage/tmp/**
|
!**/coverage/tmp/**
|
||||||
retention-days: 7
|
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!"
|
|
|
@ -80,7 +80,9 @@ export async function authCallbackHandler(
|
||||||
const callbackPayload = {
|
const callbackPayload = {
|
||||||
action: 'created' as const,
|
action: 'created' as const,
|
||||||
installation: {
|
installation: {
|
||||||
id: Number.parseInt(request.installation_id, 10),
|
id: Number.isNaN(Number(request.installation_id))
|
||||||
|
? 0
|
||||||
|
: Number.parseInt(request.installation_id, 10),
|
||||||
account: {
|
account: {
|
||||||
// These will be updated when the webhook arrives with full details
|
// These will be updated when the webhook arrives with full details
|
||||||
id: 0,
|
id: 0,
|
||||||
|
|
|
@ -277,12 +277,12 @@ describe('Auth Init Handler Integration Tests', () => {
|
||||||
const metadata = JSON.parse(storedState.description);
|
const metadata = JSON.parse(storedState.description);
|
||||||
expect(metadata.expiresAt).toBeTruthy();
|
expect(metadata.expiresAt).toBeTruthy();
|
||||||
|
|
||||||
// Should expire in 15 minutes
|
// Should expire in 10 minutes
|
||||||
const expiresAt = new Date(metadata.expiresAt);
|
const expiresAt = new Date(metadata.expiresAt);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diffMinutes = (expiresAt.getTime() - now.getTime()) / (1000 * 60);
|
const diffMinutes = (expiresAt.getTime() - now.getTime()) / (1000 * 60);
|
||||||
expect(diffMinutes).toBeGreaterThan(14);
|
expect(diffMinutes).toBeGreaterThan(9);
|
||||||
expect(diffMinutes).toBeLessThan(16);
|
expect(diffMinutes).toBeLessThan(11);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -99,7 +99,7 @@ describe('webhookHandler', () => {
|
||||||
const result = await webhookHandler(suspendedPayload);
|
const result = await webhookHandler(suspendedPayload);
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
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 () => {
|
it('should handle installation unsuspended', async () => {
|
||||||
|
@ -116,7 +116,7 @@ describe('webhookHandler', () => {
|
||||||
const result = await webhookHandler(unsuspendedPayload);
|
const result = await webhookHandler(unsuspendedPayload);
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
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 () => {
|
it('should throw 400 for created action without org context', async () => {
|
||||||
|
|
|
@ -26,10 +26,17 @@ export async function webhookHandler(
|
||||||
userId: userId || '',
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
integration: result,
|
integration: result,
|
||||||
message: `Installation ${payload.action} successfully`,
|
message: actionMessages[payload.action] || `Installation ${payload.action} successfully`,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to handle installation callback:', 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 { App } from 'octokit';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { createGitHubApp, getGitHubAppCredentials } from './github-app';
|
import { createGitHubApp, getGitHubAppCredentials } from './github-app';
|
||||||
|
@ -86,7 +85,7 @@ describe('github-app', () => {
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
expect(() => getGitHubAppCredentials()).toThrow(
|
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';
|
process.env.GITHUB_WEBHOOK_SECRET = 'test';
|
||||||
|
|
||||||
// Act & Assert
|
// 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
|
// Decode the private key from base64
|
||||||
let privateKey: string;
|
let privateKey: string;
|
||||||
try {
|
try {
|
||||||
// Check if it's valid base64 first
|
// Check if it's valid base64 first (allow whitespace which will be trimmed)
|
||||||
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(privateKeyBase64)) {
|
const trimmedBase64 = privateKeyBase64.trim();
|
||||||
|
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(trimmedBase64)) {
|
||||||
throw new Error('Invalid base64 format');
|
throw new Error('Invalid base64 format');
|
||||||
}
|
}
|
||||||
privateKey = Buffer.from(privateKeyBase64, 'base64').toString('utf-8');
|
privateKey = Buffer.from(trimmedBase64, 'base64').toString('utf-8');
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
throw createGitHubError(
|
throw createGitHubError(
|
||||||
GitHubErrorCode.APP_CONFIGURATION_ERROR,
|
GitHubErrorCode.APP_CONFIGURATION_ERROR,
|
||||||
|
@ -50,11 +51,11 @@ export function getGitHubAppCredentials(): {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the private key format
|
// Validate the private key format (support both RSA and PKCS#8 formats)
|
||||||
if (!privateKey.includes('BEGIN RSA PRIVATE KEY')) {
|
if (!privateKey.includes('BEGIN RSA PRIVATE KEY') && !privateKey.includes('BEGIN PRIVATE KEY')) {
|
||||||
throw createGitHubError(
|
throw createGitHubError(
|
||||||
GitHubErrorCode.APP_CONFIGURATION_ERROR,
|
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
|
// 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
|
// Create new integration
|
||||||
|
|
|
@ -22,6 +22,8 @@ export async function storeInstallationState(
|
||||||
data: InstallationState
|
data: InstallationState
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const key = generateStateVaultKey(state);
|
const key = generateStateVaultKey(state);
|
||||||
|
const expirationTime = new Date(Date.now() + 10 * 60 * 1000).toISOString();
|
||||||
|
const description = `GitHub OAuth state expires at ${expirationTime}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if state already exists (shouldn't happen with random generation)
|
// Check if state already exists (shouldn't happen with random generation)
|
||||||
|
@ -32,13 +34,13 @@ export async function storeInstallationState(
|
||||||
id: existing.id,
|
id: existing.id,
|
||||||
secret: JSON.stringify(data),
|
secret: JSON.stringify(data),
|
||||||
name: key,
|
name: key,
|
||||||
description: `GitHub OAuth state expires at ${new Date(Date.now() + 10 * 60 * 1000).toISOString()}`,
|
description,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await createSecret({
|
await createSecret({
|
||||||
secret: JSON.stringify(data),
|
secret: JSON.stringify(data),
|
||||||
name: key,
|
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)
|
// 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 createdAt = new Date(data.createdAt);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const tenMinutes = 10 * 60 * 1000;
|
const tenMinutes = 10 * 60 * 1000;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { deleteSecret, getSecretByName } from '@buster/database';
|
import { deleteSecret, getSecretByName } from '@buster/database';
|
||||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
import { afterEach, describe, expect, it } from 'vitest';
|
||||||
import {
|
import {
|
||||||
deleteInstallationToken,
|
deleteInstallationToken,
|
||||||
generateTokenVaultKey,
|
generateTokenVaultKey,
|
||||||
|
|
|
@ -91,13 +91,13 @@ export async function retrieveInstallationToken(
|
||||||
? (JSON.parse(secret.description) as TokenMetadata)
|
? (JSON.parse(secret.description) as TokenMetadata)
|
||||||
: {
|
: {
|
||||||
installationId,
|
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 {
|
} catch {
|
||||||
// If description is not valid JSON, create minimal metadata
|
// If description is not valid JSON, create minimal metadata
|
||||||
metadata = {
|
metadata = {
|
||||||
installationId,
|
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 () => {
|
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));
|
await db.delete(githubIntegrations).where(eq(githubIntegrations.organizationId, testOrgId));
|
||||||
|
|
||||||
// Clean up test user
|
// Clean up test user
|
||||||
|
@ -103,7 +103,7 @@ describe('getGithubIntegrationByInstallationId', () => {
|
||||||
await db.delete(users).where(eq(users.id, testUserId));
|
await db.delete(users).where(eq(users.id, testUserId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up test organization
|
// Clean up test organization last
|
||||||
if (testOrgId) {
|
if (testOrgId) {
|
||||||
await db.delete(organizations).where(eq(organizations.id, testOrgId));
|
await db.delete(organizations).where(eq(organizations.id, testOrgId));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
import { and, eq, isNull } from 'drizzle-orm';
|
import { and, eq, isNull } from 'drizzle-orm';
|
||||||
|
import type { InferSelectModel } from 'drizzle-orm';
|
||||||
import { db } from '../../connection';
|
import { db } from '../../connection';
|
||||||
import { githubIntegrations } from '../../schema';
|
import { githubIntegrations } from '../../schema';
|
||||||
|
|
||||||
|
type GitHubIntegration = InferSelectModel<typeof githubIntegrations>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get GitHub integration by installation ID
|
* 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
|
const [integration] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(githubIntegrations)
|
.from(githubIntegrations)
|
||||||
|
|
|
@ -24,6 +24,7 @@ node_modules/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Environment files
|
# Environment files
|
||||||
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|
||||||
|
|
|
@ -35,5 +35,10 @@
|
||||||
"@buster/server-shared": "workspace:*",
|
"@buster/server-shared": "workspace:*",
|
||||||
"octokit": "^5.0.3",
|
"octokit": "^5.0.3",
|
||||||
"zod": "^3.23.8"
|
"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
|
// 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
|
// Making them optional for build time to allow packages to be built without GitHub setup
|
||||||
const requiredEnv = {
|
const requiredEnv = {
|
||||||
// NODE_ENV is optional - will default to 'development' if not set
|
// GitHub App configuration (required for runtime)
|
||||||
// GitHub variables are optional at build time but required at 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
|
// Validate environment variables
|
||||||
|
|
|
@ -38,11 +38,12 @@ export function getGitHubAppCredentials(): {
|
||||||
// Decode the private key from base64
|
// Decode the private key from base64
|
||||||
let privateKey: string;
|
let privateKey: string;
|
||||||
try {
|
try {
|
||||||
// Check if it's valid base64 first
|
// Check if it's valid base64 first (allow whitespace which will be trimmed)
|
||||||
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(privateKeyBase64)) {
|
const trimmedBase64 = privateKeyBase64.trim();
|
||||||
|
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(trimmedBase64)) {
|
||||||
throw new Error('Invalid base64 format');
|
throw new Error('Invalid base64 format');
|
||||||
}
|
}
|
||||||
privateKey = Buffer.from(privateKeyBase64, 'base64').toString('utf-8');
|
privateKey = Buffer.from(trimmedBase64, 'base64').toString('utf-8');
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
throw createGitHubError(
|
throw createGitHubError(
|
||||||
GitHubErrorCode.APP_CONFIGURATION_ERROR,
|
GitHubErrorCode.APP_CONFIGURATION_ERROR,
|
||||||
|
@ -50,11 +51,11 @@ export function getGitHubAppCredentials(): {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the private key format
|
// Validate the private key format (support both RSA and PKCS#8 formats)
|
||||||
if (!privateKey.includes('BEGIN RSA PRIVATE KEY')) {
|
if (!privateKey.includes('BEGIN RSA PRIVATE KEY') && !privateKey.includes('BEGIN PRIVATE KEY')) {
|
||||||
throw createGitHubError(
|
throw createGitHubError(
|
||||||
GitHubErrorCode.APP_CONFIGURATION_ERROR,
|
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
|
// GitHub library utilities
|
||||||
export const howdy = () => {
|
// Additional utility functions can be added here as needed
|
||||||
return 'Hello from @buster/github!';
|
|
||||||
};
|
|
||||||
|
|
|
@ -115,13 +115,13 @@ export async function retrieveInstallationToken(
|
||||||
? (JSON.parse(secret.description) as TokenMetadata)
|
? (JSON.parse(secret.description) as TokenMetadata)
|
||||||
: {
|
: {
|
||||||
installationId,
|
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 {
|
} catch {
|
||||||
// If description is not valid JSON, create minimal metadata
|
// If description is not valid JSON, create minimal metadata
|
||||||
metadata = {
|
metadata = {
|
||||||
installationId,
|
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 { createHmac } from 'node:crypto';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { skipIfNoGitHubCredentials } from '../../../../apps/server/src/api/v2/github/test-helpers/github-test-setup';
|
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('GitHub Webhook Service Integration Tests', () => {
|
||||||
describe('Webhook Signature Verification', () => {
|
describe('Webhook Signature Verification', () => {
|
||||||
|
@ -30,7 +30,7 @@ describe('GitHub Webhook Service Integration Tests', () => {
|
||||||
const signature = `sha256=${createHmac('sha256', webhookSecret).update(payloadString).digest('hex')}`;
|
const signature = `sha256=${createHmac('sha256', webhookSecret).update(payloadString).digest('hex')}`;
|
||||||
|
|
||||||
// Should verify successfully
|
// Should verify successfully
|
||||||
const isValid = verifyWebhookSignature(payloadString, signature, webhookSecret);
|
const isValid = verifyGitHubWebhookSignature(payloadString, signature);
|
||||||
expect(isValid).toBe(true);
|
expect(isValid).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ describe('GitHub Webhook Service Integration Tests', () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const signature of wrongFormats) {
|
for (const signature of wrongFormats) {
|
||||||
const isValid = verifyWebhookSignature(payloadString, signature, webhookSecret);
|
const isValid = verifyGitHubWebhookSignature(payloadString, signature);
|
||||||
expect(isValid).toBe(false);
|
expect(isValid).toBe(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -114,7 +114,7 @@ describe('GitHub Webhook Service Integration Tests', () => {
|
||||||
const payloadString = JSON.stringify(payload);
|
const payloadString = JSON.stringify(payload);
|
||||||
const signature = `sha256=${createHmac('sha256', webhookSecret).update(payloadString).digest('hex')}`;
|
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);
|
expect(isValid).toBe(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -132,7 +132,7 @@ describe('GitHub Webhook Service Integration Tests', () => {
|
||||||
|
|
||||||
// Verify multiple times - should always return same result
|
// Verify multiple times - should always return same result
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
const isValid = verifyWebhookSignature(payloadString, signature, webhookSecret);
|
const isValid = verifyGitHubWebhookSignature(payloadString, signature);
|
||||||
expect(isValid).toBe(true);
|
expect(isValid).toBe(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -241,7 +241,7 @@ describe('GitHub Webhook Service Integration Tests', () => {
|
||||||
const payloadString = JSON.stringify(largePayload);
|
const payloadString = JSON.stringify(largePayload);
|
||||||
const signature = `sha256=${createHmac('sha256', webhookSecret).update(payloadString).digest('hex')}`;
|
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);
|
expect(isValid).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { RuntimeContext } from '@mastra/core/runtime-context';
|
||||||
|
|
||||||
// Option 1: Create sandbox with GitHub token
|
// Option 1: Create sandbox with GitHub token
|
||||||
const runtimeContext = new RuntimeContext();
|
const runtimeContext = new RuntimeContext();
|
||||||
const githubToken = await fetchGitHubToken(organizationId);
|
const githubToken = await getInstallationTokenByOrgId(organizationId);
|
||||||
const sandbox = await createSandboxWithGitHubToken(runtimeContext, githubToken);
|
const sandbox = await createSandboxWithGitHubToken(runtimeContext, githubToken);
|
||||||
|
|
||||||
// Option 2: Add token to existing context
|
// Option 2: Add token to existing context
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { z } from 'zod';
|
||||||
// GitHub App Installation Webhook Payload
|
// GitHub App Installation Webhook Payload
|
||||||
// Received when a GitHub App is installed, deleted, suspended, or unsuspended
|
// Received when a GitHub App is installed, deleted, suspended, or unsuspended
|
||||||
export const InstallationCallbackSchema = z.object({
|
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({
|
installation: z.object({
|
||||||
id: z.number(), // GitHub installation ID
|
id: z.number(), // GitHub installation ID
|
||||||
account: z.object({
|
account: z.object({
|
||||||
|
|
|
@ -22,7 +22,7 @@ export type InstallationCallbackResponse = z.infer<typeof InstallationCallbackRe
|
||||||
// Response containing installation access token
|
// Response containing installation access token
|
||||||
export const InstallationTokenResponseSchema = z.object({
|
export const InstallationTokenResponseSchema = z.object({
|
||||||
token: z.string(),
|
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" }
|
permissions: z.record(z.string()).optional(), // e.g., { "contents": "read", "issues": "write" }
|
||||||
repository_selection: z.enum(['all', 'selected']).optional(),
|
repository_selection: z.enum(['all', 'selected']).optional(),
|
||||||
repositories: z
|
repositories: z
|
||||||
|
@ -49,8 +49,8 @@ export const GetGitHubIntegrationResponseSchema = z.object({
|
||||||
github_org_name: z.string(),
|
github_org_name: z.string(),
|
||||||
github_org_id: z.string(),
|
github_org_id: z.string(),
|
||||||
installation_id: z.string(),
|
installation_id: z.string(),
|
||||||
installed_at: z.string(),
|
installed_at: z.string().datetime(),
|
||||||
last_used_at: z.string().optional(),
|
last_used_at: z.string().datetime().optional(),
|
||||||
repository_count: z.number().optional(),
|
repository_count: z.number().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|
Loading…
Reference in New Issue