Compare commits

...

4 commits

Author SHA1 Message Date
msukkari
095474a901 update perm syncing docs
Some checks are pending
Publish to ghcr / build (linux/arm64, blacksmith-8vcpu-ubuntu-2204-arm) (push) Waiting to run
Publish to ghcr / build (linux/amd64, blacksmith-4vcpu-ubuntu-2404) (push) Waiting to run
Publish to ghcr / merge (push) Blocked by required conditions
Update Roadmap Released / update (push) Waiting to run
2025-12-11 06:46:20 -08:00
Brendan Kellam
d63f3cf9d9
chore(web): Improve error messages for file loading errors (#665)
Some checks failed
Publish to ghcr / build (linux/amd64, blacksmith-4vcpu-ubuntu-2404) (push) Has been cancelled
Publish to ghcr / build (linux/arm64, blacksmith-8vcpu-ubuntu-2204-arm) (push) Has been cancelled
Update Roadmap Released / update (push) Has been cancelled
Publish to ghcr / merge (push) Has been cancelled
2025-12-05 11:58:19 -08:00
Cade 🐀
3d85a0595c
fix: add support for anyuid to Dockerfile (#658)
Some checks are pending
Publish to ghcr / build (linux/amd64, blacksmith-4vcpu-ubuntu-2404) (push) Waiting to run
Publish to ghcr / build (linux/arm64, blacksmith-8vcpu-ubuntu-2204-arm) (push) Waiting to run
Publish to ghcr / merge (push) Blocked by required conditions
Update Roadmap Released / update (push) Waiting to run
* fix: add support for anyuid to Dockerfile

* changelog

---------

Co-authored-by: Cade Schlaefli <cade.schlaefli@mouser.com>
Co-authored-by: Brendan Kellam <bshizzle1234@gmail.com>
2025-12-04 22:29:23 -08:00
Brian Phillips
84cf524d84
Add GHES support to the review agent (#611)
* add support for GHES to the review agent

* fix throttling types

---------

Co-authored-by: Brendan Kellam <bshizzle1234@gmail.com>
2025-12-04 22:08:24 -08:00
6 changed files with 86 additions and 24 deletions

View file

@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Fixed
- Fixed review agent so that it works with GHES instances [#611](https://github.com/sourcebot-dev/sourcebot/pull/611)
### Added
- Added support for arbitrary user IDs required for OpenShift. [#658](https://github.com/sourcebot-dev/sourcebot/pull/658)
### Updated
- Improved error messages in file source api. [#665](https://github.com/sourcebot-dev/sourcebot/pull/665)
## [4.10.2] - 2025-12-04 ## [4.10.2] - 2025-12-04
### Fixed ### Fixed

View file

@ -195,6 +195,7 @@ RUN addgroup -g $GID sourcebot && \
adduser -D -u $UID -h /app -S sourcebot && \ adduser -D -u $UID -h /app -S sourcebot && \
adduser sourcebot postgres && \ adduser sourcebot postgres && \
adduser sourcebot redis && \ adduser sourcebot redis && \
chown -R sourcebot /app && \
adduser sourcebot node && \ adduser sourcebot node && \
mkdir /var/log/sourcebot && \ mkdir /var/log/sourcebot && \
chown sourcebot /var/log/sourcebot chown sourcebot /var/log/sourcebot
@ -244,7 +245,12 @@ RUN mkdir -p /run/postgresql && \
chown -R postgres:postgres /run/postgresql && \ chown -R postgres:postgres /run/postgresql && \
chmod 775 /run/postgresql chmod 775 /run/postgresql
RUN chown -R sourcebot:sourcebot /data # Make app directory accessible to both root and sourcebot user
RUN chown -R sourcebot /app \
&& chgrp -R 0 /app \
&& chmod -R g=u /app
# Make data directory accessible to both root and sourcebot user
RUN chown -R sourcebot /data
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY prefix-output.sh ./prefix-output.sh COPY prefix-output.sh ./prefix-output.sh

View file

@ -1,14 +1,11 @@
--- ---
title: "Permission syncing" title: "Permission syncing"
sidebarTitle: "Permission syncing" sidebarTitle: "Permission syncing"
tag: "experimental"
--- ---
import LicenseKeyRequired from '/snippets/license-key-required.mdx' import LicenseKeyRequired from '/snippets/license-key-required.mdx'
import ExperimentalFeatureWarning from '/snippets/experimental-feature-warning.mdx'
<LicenseKeyRequired /> <LicenseKeyRequired />
<ExperimentalFeatureWarning />
# Overview # Overview

View file

@ -38,8 +38,8 @@ const auditService = getAuditService();
/** /**
* "Service Error Wrapper". * "Service Error Wrapper".
* *
* Captures any thrown exceptions and converts them to a unexpected * Captures any thrown exceptions, logs them to the console and Sentry,
* service error. Also logs them with Sentry. * and returns a generic unexpected service error.
*/ */
export const sew = async <T>(fn: () => Promise<T>): Promise<T | ServiceError> => { export const sew = async <T>(fn: () => Promise<T>): Promise<T | ServiceError> => {
try { try {
@ -52,10 +52,6 @@ export const sew = async <T>(fn: () => Promise<T>): Promise<T | ServiceError> =>
return e.serviceError; return e.serviceError;
} }
if (e instanceof Error) {
return unexpectedError(e.message);
}
return unexpectedError(`An unexpected error occurred. Please try again later.`); return unexpectedError(`An unexpected error occurred. Please try again later.`);
} }
} }

View file

@ -22,8 +22,12 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName }: CodePre
getRepoInfoByName(repoName), getRepoInfoByName(repoName),
]); ]);
if (isServiceError(fileSourceResponse) || isServiceError(repoInfoResponse)) { if (isServiceError(fileSourceResponse)) {
return <div>Error loading file source</div> return <div>Error loading file source: {fileSourceResponse.message}</div>
}
if (isServiceError(repoInfoResponse)) {
return <div>Error loading repo info: {repoInfoResponse.message}</div>
} }
const codeHostInfo = getCodeHostInfoForRepo({ const codeHostInfo = getCodeHostInfoForRepo({

View file

@ -6,28 +6,32 @@ import { WebhookEventDefinition} from "@octokit/webhooks/types";
import { EndpointDefaults } from "@octokit/types"; import { EndpointDefaults } from "@octokit/types";
import { env } from "@sourcebot/shared"; import { env } from "@sourcebot/shared";
import { processGitHubPullRequest } from "@/features/agents/review-agent/app"; import { processGitHubPullRequest } from "@/features/agents/review-agent/app";
import { throttling } from "@octokit/plugin-throttling"; import { throttling, type ThrottlingOptions } from "@octokit/plugin-throttling";
import fs from "fs"; import fs from "fs";
import { GitHubPullRequest } from "@/features/agents/review-agent/types"; import { GitHubPullRequest } from "@/features/agents/review-agent/types";
import { createLogger } from "@sourcebot/shared"; import { createLogger } from "@sourcebot/shared";
const logger = createLogger('github-webhook'); const logger = createLogger('github-webhook');
let githubApp: App | undefined; const DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com";
type GitHubAppBaseOptions = Omit<ConstructorParameters<typeof App>[0], "Octokit"> & { throttle: ThrottlingOptions };
let githubAppBaseOptions: GitHubAppBaseOptions | undefined;
const githubAppCache = new Map<string, App>();
if (env.GITHUB_REVIEW_AGENT_APP_ID && env.GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET && env.GITHUB_REVIEW_AGENT_APP_PRIVATE_KEY_PATH) { if (env.GITHUB_REVIEW_AGENT_APP_ID && env.GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET && env.GITHUB_REVIEW_AGENT_APP_PRIVATE_KEY_PATH) {
try { try {
const privateKey = fs.readFileSync(env.GITHUB_REVIEW_AGENT_APP_PRIVATE_KEY_PATH, "utf8"); const privateKey = fs.readFileSync(env.GITHUB_REVIEW_AGENT_APP_PRIVATE_KEY_PATH, "utf8");
const throttledOctokit = Octokit.plugin(throttling); githubAppBaseOptions = {
githubApp = new App({
appId: env.GITHUB_REVIEW_AGENT_APP_ID, appId: env.GITHUB_REVIEW_AGENT_APP_ID,
privateKey: privateKey, privateKey,
webhooks: { webhooks: {
secret: env.GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET, secret: env.GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET,
}, },
Octokit: throttledOctokit,
throttle: { throttle: {
onRateLimit: (retryAfter: number, options: Required<EndpointDefaults>, octokit: Octokit, retryCount: number) => { enabled: true,
onRateLimit: (retryAfter, _options, _octokit, retryCount) => {
if (retryCount > 3) { if (retryCount > 3) {
logger.warn(`Rate limit exceeded: ${retryAfter} seconds`); logger.warn(`Rate limit exceeded: ${retryAfter} seconds`);
return false; return false;
@ -35,13 +39,55 @@ if (env.GITHUB_REVIEW_AGENT_APP_ID && env.GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET
return true; return true;
}, },
} onSecondaryRateLimit: (_retryAfter, options) => {
}); // no retries on secondary rate limits
logger.warn(`SecondaryRateLimit detected for ${options.method} ${options.url}`);
}
},
};
} catch (error) { } catch (error) {
logger.error(`Error initializing GitHub app: ${error}`); logger.error(`Error initializing GitHub app: ${error}`);
} }
} }
const normalizeGithubApiBaseUrl = (baseUrl?: string) => {
if (!baseUrl) {
return DEFAULT_GITHUB_API_BASE_URL;
}
return baseUrl.replace(/\/+$/, "");
};
const resolveGithubApiBaseUrl = (headers: Record<string, string>) => {
const enterpriseHost = headers["x-github-enterprise-host"];
if (enterpriseHost) {
return normalizeGithubApiBaseUrl(`https://${enterpriseHost}/api/v3`);
}
return DEFAULT_GITHUB_API_BASE_URL;
};
const getGithubAppForBaseUrl = (baseUrl: string) => {
if (!githubAppBaseOptions) {
return undefined;
}
const normalizedBaseUrl = normalizeGithubApiBaseUrl(baseUrl);
const cachedApp = githubAppCache.get(normalizedBaseUrl);
if (cachedApp) {
return cachedApp;
}
const OctokitWithBaseUrl = Octokit.plugin(throttling).defaults({ baseUrl: normalizedBaseUrl });
const app = new App({
...githubAppBaseOptions,
Octokit: OctokitWithBaseUrl,
});
githubAppCache.set(normalizedBaseUrl, app);
return app;
};
function isPullRequestEvent(eventHeader: string, payload: unknown): payload is WebhookEventDefinition<"pull-request-opened"> | WebhookEventDefinition<"pull-request-synchronize"> { function isPullRequestEvent(eventHeader: string, payload: unknown): payload is WebhookEventDefinition<"pull-request-opened"> | WebhookEventDefinition<"pull-request-synchronize"> {
return eventHeader === "pull_request" && typeof payload === "object" && payload !== null && "action" in payload && typeof payload.action === "string" && (payload.action === "opened" || payload.action === "synchronize"); return eventHeader === "pull_request" && typeof payload === "object" && payload !== null && "action" in payload && typeof payload.action === "string" && (payload.action === "opened" || payload.action === "synchronize");
} }
@ -52,12 +98,16 @@ function isIssueCommentEvent(eventHeader: string, payload: unknown): payload is
export const POST = async (request: NextRequest) => { export const POST = async (request: NextRequest) => {
const body = await request.json(); const body = await request.json();
const headers = Object.fromEntries(request.headers.entries()); const headers = Object.fromEntries(Array.from(request.headers.entries(), ([key, value]) => [key.toLowerCase(), value]));
const githubEvent = headers['x-github-event'] || headers['X-GitHub-Event']; const githubEvent = headers['x-github-event'];
if (githubEvent) { if (githubEvent) {
logger.info('GitHub event received:', githubEvent); logger.info('GitHub event received:', githubEvent);
const githubApiBaseUrl = resolveGithubApiBaseUrl(headers);
logger.debug('Using GitHub API base URL for event', { githubApiBaseUrl });
const githubApp = getGithubAppForBaseUrl(githubApiBaseUrl);
if (!githubApp) { if (!githubApp) {
logger.warn('Received GitHub webhook event but GitHub app env vars are not set'); logger.warn('Received GitHub webhook event but GitHub app env vars are not set');
return Response.json({ status: 'ok' }); return Response.json({ status: 'ok' });
@ -113,4 +163,4 @@ export const POST = async (request: NextRequest) => {
} }
return Response.json({ status: 'ok' }); return Response.json({ status: 'ok' });
} }