Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ next-env.d.ts
/dev/*/.dev-logs/
/dev/fixtures/
/dev/*/fixtures/
!/dev/review/fixtures/
!/dev/review/fixtures/*.json
dev-debug-request-logs/*
**/dev-debug-request-logs/
/dev/logs/
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/api/webhooks/github/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { NextRequest, after } from 'next/server';
import { captureException } from '@sentry/nextjs';
import { bot } from '@/lib/bot';
import { handleGitHubWebhook } from '@/lib/integrations/platforms/github/webhook-handler';

function cloneGitHubRequest(request: NextRequest, rawBody: string) {
Expand All @@ -24,6 +23,7 @@ export async function POST(request: NextRequest) {

after(async () => {
try {
const { bot } = await import('@/lib/bot');
const response = await bot.webhooks.github(botRequest, {
waitUntil: task => after(() => task),
});
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/lib/code-reviews/local-dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const LOCAL_TRUE_VALUES = new Set(['1', 'true', 'yes', 'on']);

export function isLocalCodeReviewFakeProviderEnabled(): boolean {
if (process.env.NODE_ENV === 'production') return false;

const value = process.env.CODE_REVIEW_LOCAL_FAKE_PROVIDER?.trim().toLowerCase();
return value !== undefined && LOCAL_TRUE_VALUES.has(value);
}
437 changes: 234 additions & 203 deletions apps/web/src/lib/code-reviews/triggers/prepare-review-payload.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import { buildInstallationData } from '../webhook-helpers';
import { INTEGRATION_STATUS, PLATFORM } from '@/lib/integrations/core/constants';
import { logExceptInTest } from '@/lib/utils.server';
import { captureException } from '@sentry/nextjs';
import { bot } from '@/lib/bot';
import { unlinkTeamKiloUsers } from '@/lib/bot-identity';

/**
Expand Down Expand Up @@ -124,6 +123,7 @@ export async function handleInstallationDeleted(payload: InstallationDeletedPayl
);

try {
const { bot } = await import('@/lib/bot');
await bot.initialize();
await unlinkTeamKiloUsers(bot.getState(), PLATFORM.GITHUB, installationIdStr);
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { updateCheckRunId } from '@/lib/code-reviews/db/code-reviews';
import { resolvePullRequestCheckoutRef } from './pull-request-checkout-ref';
import { APP_URL } from '@/lib/constants';
import { getCodeReviewActionRequiredState } from '@/lib/code-reviews/action-required';
import { isLocalCodeReviewFakeProviderEnabled } from '@/lib/code-reviews/local-dev';

/**
* GitHub Pull Request Event Handler
Expand Down Expand Up @@ -155,6 +156,7 @@ export async function handlePullRequestCodeReview(
const appType = integration.github_app_type ?? 'standard';
const headFullName = checkoutRef.headRepoFullName ?? repository.full_name;
const [headOwner, headRepoName] = headFullName.split('/');
const useLocalFakeProvider = isLocalCodeReviewFakeProviderEnabled();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Local fake mode still reaches GitHub on synchronize

useLocalFakeProvider is only consulted after shouldSkipSynchronizeForMergeCommit() runs. On pull_request.synchronize, that helper still calls isMergeCommit(), which generates an installation token and hits git.getCommit. This breaks the CODE_REVIEW_LOCAL_FAKE_PROVIDER=1 contract of avoiding real provider access, so replaying captured synchronize payloads can still fail against the seeded fake integration.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


// 4. Skip merge commits on synchronize (e.g. merging base branch into feature branch).
// Runs before cancellation so that an in-flight review at an earlier SHA is preserved:
Expand Down Expand Up @@ -320,7 +322,7 @@ export async function handlePullRequestCodeReview(
const [repoOwner, repoName] = repository.full_name.split('/');

// 8. Create GitHub Check Run (PR gate) — skip for lite (read-only) app
if (appType !== 'lite') {
if (appType !== 'lite' && !useLocalFakeProvider) {
let checkRunId: number | undefined;
try {
const detailsUrl = `${APP_URL}/code-reviews/${reviewId}`;
Expand Down Expand Up @@ -369,18 +371,26 @@ export async function handlePullRequestCodeReview(
}

// 9. Post 👀 reaction to show Kilo is reviewing
try {
await addReactionToPR(
integration.platform_installation_id as string,
repoOwner,
repoName,
pull_request.number,
'eyes'
);
logExceptInTest(`Added eyes reaction to ${repository.full_name}#${pull_request.number}`);
} catch (reactionError) {
// Non-blocking - log but don't fail the review
logExceptInTest('Failed to add eyes reaction:', reactionError);
if (!useLocalFakeProvider) {
try {
await addReactionToPR(
integration.platform_installation_id as string,
repoOwner,
repoName,
pull_request.number,
'eyes'
);
logExceptInTest(`Added eyes reaction to ${repository.full_name}#${pull_request.number}`);
} catch (reactionError) {
// Non-blocking - log but don't fail the review
logExceptInTest('Failed to add eyes reaction:', reactionError);
}
} else {
logExceptInTest('Skipping GitHub provider side effects for local fake review', {
reviewId,
repo: repository.full_name,
prNumber: pull_request.number,
});
}

// 10. Try to dispatch pending reviews (including this new one)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
getValidGitLabToken,
} from '@/lib/integrations/gitlab-service';
import { APP_URL } from '@/lib/constants';
import { isLocalCodeReviewFakeProviderEnabled } from '@/lib/code-reviews/local-dev';

/**
* Handles merge request events that trigger code review
Expand Down Expand Up @@ -321,6 +322,7 @@ export async function handleMergeRequestCodeReview(

// 8. Resolve checkout ref (fork MRs use refs/merge-requests/<iid>/head)
const { checkoutRef } = resolveMergeRequestCheckoutRef(payload);
const useLocalFakeProvider = isLocalCodeReviewFakeProviderEnabled();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Local fake mode still reaches GitLab on update paths

useLocalFakeProvider is only computed after the earlier update-path provider calls. merge_request.update still runs getValidGitLabToken() and isMergeCommit(), and the cancelled-review cleanup above can still call getOrCreateProjectAccessToken() plus setCommitStatus() before this guard is checked. That means the fake-provider workflow can still require a real GitLab token and instance even though this mode is documented as avoiding provider API access.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


// 9. Create review record (session_id will be updated async)
const reviewId = await createCodeReview({
Expand All @@ -341,7 +343,7 @@ export async function handleMergeRequestCodeReview(
logExceptInTest(`Created code review ${reviewId} for ${project.path_with_namespace}!${mr.iid}`);

// 10. Post 👀 reaction and set commit status (using PrAT for bot identity)
if (fullIntegration) {
if (fullIntegration && !useLocalFakeProvider) {
try {
const pratToken = await getOrCreateProjectAccessToken(fullIntegration, project.id);
logExceptInTest(`Got PrAT for project ${project.path_with_namespace}`, {
Expand Down Expand Up @@ -379,6 +381,12 @@ export async function handleMergeRequestCodeReview(
error: reactionError instanceof Error ? reactionError.message : String(reactionError),
});
}
} else if (useLocalFakeProvider) {
logExceptInTest('Skipping GitLab provider side effects for local fake review', {
reviewId,
project: project.path_with_namespace,
mrIid: mr.iid,
});
}

// 11. Try to dispatch pending reviews (including this new one)
Expand Down
124 changes: 86 additions & 38 deletions dev/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,105 @@
# Dev Script Guide For AI Agents

These webhook test scripts intentionally use generic placeholder JSON.
They are not expected to represent valid production payloads.
Preferred workflow: capture a real webhook from smee.io, then ask an AI to replace or provide that payload to the script.
Use the local fake-provider review flow first when debugging webhook-to-review behavior without a real repository. It exercises the Next.js webhook route, database routing, code-review Worker, reviewer Durable Object, cloud-agent-next Durable Object, and Docker sandbox. It avoids real GitHub/GitLab API and clone dependencies by using explicit local-only flags.

## Script Map
## Review Webhook Flow

- Review flow:
- `./dev/review/dev-review.sh`
- `./dev/review/test-review-webhook.sh [payload.json]`
- Auto-fix flow (`@kilo fix it`):
- `./dev/auto-fix/dev-auto-fix.sh`
- `./dev/auto-fix/test-auto-fix-webhook.sh [payload.json]`
1. Sync local dev env files after pulling changes:

## GitHub App Install Prerequisites
```bash
pnpm dev:env code-review
```

1. Ensure local app secrets are configured, especially `GITHUB_APP_WEBHOOK_SECRET` in `.env.local`.
2. Install the GitHub App on the repo/org you want to test.
3. In the GitHub App webhook settings:
2. Start the code-review stack with the local fake provider enabled:

```bash
CODE_REVIEW_LOCAL_FAKE_PROVIDER=1 KILO_PORT_OFFSET=auto pnpm dev:start --no-attach code-review
```

3. Seed fake integrations and refresh the tracked webhook fixtures:

```bash
pnpm dev:seed review:webhook-fixtures
```

- Set webhook URL to your smee channel URL: `https://smee.io/<channel-id>`.
- Set webhook secret to match your local webhook secret.
- Subscribe to required events:
- Review flow: `pull_request`.
- Auto-fix flow: `pull_request_review_comment`.
4. Send a signed GitHub pull request webhook and verify the generated review prompt reached fake-LLM:

## Forward GitHub Events Locally With smee.io
```bash
VERIFY_FAKE_LLM=1 ./dev/review/test-review-webhook.sh --github
```

1. Create a channel at [smee.io](https://smee.io).
2. Run the relay locally:
5. Send a GitLab merge request webhook and verify the generated review prompt reached fake-LLM:

```bash
VERIFY_FAKE_LLM=1 ./dev/review/test-review-webhook.sh --gitlab
```

Re-run `pnpm dev:seed review:webhook-fixtures` before repeating a fixture webhook. Review rows are unique by repo, PR/MR number, and head SHA, so reseeding clears the previous fixture review and webhook dedupe rows.

## What The Fake Flag Does

`CODE_REVIEW_LOCAL_FAKE_PROVIDER=1` is local-only and should not be used for production-like provider API testing.

| Component | Behavior |
|---|---|
| Next.js webhook/review code | Skips GitHub/GitLab provider token reads, check/status writes, reactions, comments, repository size reads, and `REVIEW.md` reads. |
| Next.js and code-review Worker | The dev runner injects matching local `CALLBACK_TOKEN_SECRET` values so callback status updates work without interactive `dev:env`. |
| cloud-agent-next | Wrangler receives `KILOCODE_DEV_FAKE_REPOSITORY=1` and creates a synthetic git origin/ref inside the sandbox instead of cloning GitHub/GitLab. |
| fake-LLM | Wrangler receives `KILO_OPENROUTER_BASE=http://localhost:<fake-llm-port>/api`; seeded prompts include `__fake__:idle` so no `gh`/`glab` CLI call is attempted. |
| test script | Discovers the active Next.js and fake-LLM ports with `pnpm dev:status --json`; never assumes port `3000`. |

## Fixture Files

The review webhook fixtures are tracked canonical examples:

- `dev/review/fixtures/github-pull-request-opened.json`
- `dev/review/fixtures/gitlab-merge-request-open.json`

`pnpm dev:seed review:webhook-fixtures` refreshes these files and resets the local fixture rows. The fixtures are based on GitHub pull request and GitLab merge request webhook shapes and include the fields the local handlers require.

## Sending Custom Payloads

Use captured payloads with the same sender script:

```bash
./dev/review/test-review-webhook.sh --github /path/to/github-payload.json
./dev/review/test-review-webhook.sh --gitlab /path/to/gitlab-payload.json
```

Payloads wrapped as `{"event":"...","payload":{...}}` are unwrapped automatically. Override `EVENT_TYPE`, `WEBHOOK_URL`, `WEBHOOK_SECRET`, or `GITLAB_WEBHOOK_TOKEN` only when testing non-default routes or captured deliveries.

For GitHub, the script signs the body with `WEBHOOK_SECRET`, `GITHUB_APP_WEBHOOK_SECRET`, or `GITHUB_APP_WEBHOOK_SECRET` parsed from `.env.local`. For GitLab, the seeded token is `dev-review-gitlab-webhook-secret`.

## Real Provider Flow

Use smee.io when you intentionally need real GitHub or GitLab provider behavior, including installation token generation, check/status updates, reactions, comments, `REVIEW.md`, and real repository cloning.

1. Start the relevant local stack without `CODE_REVIEW_LOCAL_FAKE_PROVIDER=1`.
2. Create a channel at [smee.io](https://smee.io).
3. Forward GitHub webhooks locally:

```bash
npx smee-client \
--url https://smee.io/<channel-id> \
--target http://127.0.0.1:3000/api/webhooks/github
--target http://127.0.0.1:<nextjs-port>/api/webhooks/github
```

3. Keep this process running while testing.
4. Trigger a real GitHub event and capture the delivered JSON payload from smee.
4. Trigger a real provider event and save the delivered JSON payload.
5. Replay it with `./dev/review/test-review-webhook.sh --github payload.json` or the GitLab equivalent.

## How AI Agents Should Use The Test Scripts
Get `<nextjs-port>` from `pnpm dev:status --json` or `dev/logs/manifest.json`.

## Script Map

1. Start the required local services using the matching `dev` script.
2. Save the real captured webhook payload to a JSON file.
3. Run the matching test script with that file path.
4. If payload is wrapped as `{"event":"...","payload":{...}}`, scripts auto-unwrap `.payload`.
5. If no file is provided, scripts send embedded generic placeholder JSON.
- Review flow: `./dev/review/test-review-webhook.sh [--github|--gitlab] [payload.json|-]`
- Legacy fixed-port review script: `./dev/review/dev-review.sh`
- Auto-fix flow: `./dev/auto-fix/dev-auto-fix.sh`
- Auto-fix webhook sender: `./dev/auto-fix/test-auto-fix-webhook.sh [payload.json]`

## Notes
## Logs

- Generic payload mode is for scaffolding only and may fail validation or integration checks.
- Real payloads from smee are the source of truth for local webhook debugging.
- Scripts sign payloads using `WEBHOOK_SECRET` (env override supported).
- Log files are written under:
- `dev/.dev-logs/review/` for review flow
- `dev/.dev-logs/auto-fix/` for auto-fix flow
- Dev manifest: `dev/logs/manifest.json`
- Next.js logs: `dev/logs/nextjs.log`
- Code-review Worker logs: `dev/logs/cloudflare-code-review-infra.log`
- cloud-agent-next logs: `dev/logs/cloud-agent-next.log`
- fake-LLM logs: `dev/logs/fake-llm.log`
12 changes: 9 additions & 3 deletions dev/local/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,13 @@ async function cmdUp(args: string[], repoRoot: string): Promise<void> {
PATH: sessionPath,
WRANGLER_REGISTRY_PATH: wranglerRegistryPath,
};
for (const key of ['PNPM_HOME', 'COREPACK_HOME', 'npm_execpath']) {
for (const key of [
'PNPM_HOME',
'COREPACK_HOME',
'npm_execpath',
'CODE_REVIEW_LOCAL_FAKE_PROVIDER',
'KILOCODE_DEV_FAKE_REPOSITORY',
]) {
const value = process.env[key];
if (value !== undefined && value !== '') {
sessionEnv[key] = value;
Expand Down Expand Up @@ -534,7 +540,7 @@ async function cmdStatus(repoRoot: string, isJson = false): Promise<void> {
}
}

async function cmdRestart(serviceName: string, repoRoot: string): Promise<void> {
async function cmdRestart(serviceName: string): Promise<void> {
if (!services.has(serviceName)) {
console.error(`Unknown service: ${serviceName}`);
process.exit(1);
Expand Down Expand Up @@ -656,7 +662,7 @@ async function main() {
console.error('Usage: dev:restart <service>');
process.exit(1);
}
await cmdRestart(serviceName, repoRoot);
await cmdRestart(serviceName);
break;
}
case 'env':
Expand Down
45 changes: 44 additions & 1 deletion dev/local/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,11 +386,53 @@ export function getAllInfraProfiles(): string[] {
const CONTAINER_EGRESS_IMAGE_ARM64 =
'cloudflare/proxy-everything:3cb1195@sha256:78c7910f4575a511d928d7824b1cbcaec6b7c4bf4dbb3fafaeeae3104030e73c';

const LOCAL_TRUE_VALUES = new Set(['1', 'true', 'yes', 'on']);
const LOCAL_REVIEW_CALLBACK_TOKEN_SECRET = 'kilocode-local-review-callback-secret';

function containerEgressImageEnvPrefix(): string[] {
if (process.arch !== 'arm64') return [];
return ['env', `MINIFLARE_CONTAINER_EGRESS_IMAGE=${CONTAINER_EGRESS_IMAGE_ARM64}`];
}

function envFlagEnabled(name: string): boolean {
const value = process.env[name]?.trim().toLowerCase();
return value !== undefined && LOCAL_TRUE_VALUES.has(value);
}

function workerDevVarArgs(serviceName: string): string[] {
if (
!envFlagEnabled('CODE_REVIEW_LOCAL_FAKE_PROVIDER') &&
!envFlagEnabled('KILOCODE_DEV_FAKE_REPOSITORY')
) {
return [];
}

if (serviceName === 'cloudflare-code-review-infra') {
return ['--var', `CALLBACK_TOKEN_SECRET:${LOCAL_REVIEW_CALLBACK_TOKEN_SECRET}`];
}

if (serviceName !== 'cloud-agent-next') return [];

const fakeLlmPort = 8811 + portOffset;
return [
'--var',
'KILOCODE_DEV_FAKE_REPOSITORY:1',
'--var',
`KILO_OPENROUTER_BASE:http://localhost:${fakeLlmPort}/api`,
];
}

function nextjsCommand(): string[] {
if (!envFlagEnabled('CODE_REVIEW_LOCAL_FAKE_PROVIDER')) return ['pnpm', 'run', 'dev'];
return [
'env',
`CALLBACK_TOKEN_SECRET=${LOCAL_REVIEW_CALLBACK_TOKEN_SECRET}`,
'pnpm',
'run',
'dev',
];
}

function buildServiceDefs(): ServiceDef[] {
const repoRoot = path.resolve(import.meta.dirname, '../..');
const defs: ServiceDef[] = [];
Expand All @@ -405,7 +447,7 @@ function buildServiceDefs(): ServiceDef[] {
dir: 'apps/web',
port: nextjsTargetPort,
dependsOn: meta.dependsOn,
command: ['pnpm', 'run', 'dev'],
command: nextjsCommand(),
group: meta.group,
});
continue;
Expand Down Expand Up @@ -547,6 +589,7 @@ function buildServiceDefs(): ServiceDef[] {
String(inspectorPort),
'--ip',
'0.0.0.0',
...workerDevVarArgs(name),
];

defs.push({
Expand Down
Loading
Loading