Skip to content
Merged
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
4 changes: 2 additions & 2 deletions build/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2025,13 +2025,13 @@ function buildReviewConversationBlock(issueComments, commentsByPullRequest, botL
function buildReviewConversationContext(issueComments, commentsByPullRequest, botLogin) {
const entries = [];
for (const comment of issueComments) {
if (isBot(comment.user?.login, botLogin))
if (comment.isAutomatedAuthor || isBot(comment.user?.login, botLogin))
continue;
appendConversationEntry(entries, comment.user?.login, 'general PR/issue comment', comment.body, comment.createdAt, `issue:${comment.id}`);
}
for (const comments of commentsByPullRequest.values()) {
for (const comment of comments) {
if (isBot(comment.authorLogin, botLogin))
if (comment.isAutomatedAuthor || isBot(comment.authorLogin, botLogin))
continue;
const location = comment.path
? `inline review comment at ${comment.path}${comment.line ? `:${comment.line}` : ''}`
Expand Down
2 changes: 2 additions & 0 deletions build/api/src/application/ports/bugbot_issue_read_ports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export interface BugbotIssueComment {
user?: {
login?: string;
};
/** Provider-authenticated author classification; never inferred from the login. */
isAutomatedAuthor?: boolean;
createdAt?: string;
}
export interface BugbotIssueReadPort {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export type PullRequestReviewComment = {
path?: string;
line?: number;
authorLogin?: string;
/** Provider-authenticated author classification; never inferred from the login. */
isAutomatedAuthor?: boolean;
createdAt?: string;
/** Opaque identity of the submitted review that owns this comment. */
parentReviewIdentity?: string;
Expand Down
10 changes: 6 additions & 4 deletions build/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52825,13 +52825,13 @@ function buildReviewConversationBlock(issueComments, commentsByPullRequest, botL
function buildReviewConversationContext(issueComments, commentsByPullRequest, botLogin) {
const entries = [];
for (const comment of issueComments) {
if (isBot(comment.user?.login, botLogin))
if (comment.isAutomatedAuthor || isBot(comment.user?.login, botLogin))
continue;
appendConversationEntry(entries, comment.user?.login, 'general PR/issue comment', comment.body, comment.createdAt, `issue:${comment.id}`);
}
for (const comments of commentsByPullRequest.values()) {
for (const comment of comments) {
if (isBot(comment.authorLogin, botLogin))
if (comment.isAutomatedAuthor || isBot(comment.authorLogin, botLogin))
continue;
const location = comment.path
? `inline review comment at ${comment.path}${comment.line ? `:${comment.line}` : ''}`
Expand Down Expand Up @@ -67040,13 +67040,13 @@ class BugbotIssueCommentQueryRepository {
issueOrPullRequest(number: $issueNumber) {
... on Issue {
comments(last: 100, before: $cursor) {
nodes { databaseId body author { login } createdAt }
nodes { databaseId body author { login __typename } createdAt }
pageInfo { hasPreviousPage startCursor }
}
}
... on PullRequest {
comments(last: 100, before: $cursor) {
nodes { databaseId body author { login } createdAt }
nodes { databaseId body author { login __typename } createdAt }
pageInfo { hasPreviousPage startCursor }
}
}
Expand All @@ -67064,6 +67064,7 @@ class BugbotIssueCommentQueryRepository {
id: Number(comment.databaseId),
body: comment.body ?? null,
...(comment.author?.login ? { user: { login: comment.author.login } } : {}),
...(comment.author?.__typename === 'Bot' ? { isAutomatedAuthor: true } : {}),
...(comment.createdAt ? { createdAt: comment.createdAt } : {}),
}];
});
Expand Down Expand Up @@ -69326,6 +69327,7 @@ function toReviewComment(comment) {
path: comment.path,
line: comment.line ?? undefined,
authorLogin: comment.user?.login ?? undefined,
...(comment.user?.type === 'Bot' ? { isAutomatedAuthor: true } : {}),
...(comment.created_at ? { createdAt: comment.created_at } : {}),
...(comment.pull_request_review_id != null
? { parentReviewIdentity: String(comment.pull_request_review_id) }
Expand Down
10 changes: 6 additions & 4 deletions build/github_action/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52894,13 +52894,13 @@ function buildReviewConversationBlock(issueComments, commentsByPullRequest, botL
function buildReviewConversationContext(issueComments, commentsByPullRequest, botLogin) {
const entries = [];
for (const comment of issueComments) {
if (isBot(comment.user?.login, botLogin))
if (comment.isAutomatedAuthor || isBot(comment.user?.login, botLogin))
continue;
appendConversationEntry(entries, comment.user?.login, 'general PR/issue comment', comment.body, comment.createdAt, `issue:${comment.id}`);
}
for (const comments of commentsByPullRequest.values()) {
for (const comment of comments) {
if (isBot(comment.authorLogin, botLogin))
if (comment.isAutomatedAuthor || isBot(comment.authorLogin, botLogin))
continue;
const location = comment.path
? `inline review comment at ${comment.path}${comment.line ? `:${comment.line}` : ''}`
Expand Down Expand Up @@ -65412,13 +65412,13 @@ class BugbotIssueCommentQueryRepository {
issueOrPullRequest(number: $issueNumber) {
... on Issue {
comments(last: 100, before: $cursor) {
nodes { databaseId body author { login } createdAt }
nodes { databaseId body author { login __typename } createdAt }
pageInfo { hasPreviousPage startCursor }
}
}
... on PullRequest {
comments(last: 100, before: $cursor) {
nodes { databaseId body author { login } createdAt }
nodes { databaseId body author { login __typename } createdAt }
pageInfo { hasPreviousPage startCursor }
}
}
Expand All @@ -65436,6 +65436,7 @@ class BugbotIssueCommentQueryRepository {
id: Number(comment.databaseId),
body: comment.body ?? null,
...(comment.author?.login ? { user: { login: comment.author.login } } : {}),
...(comment.author?.__typename === 'Bot' ? { isAutomatedAuthor: true } : {}),
...(comment.createdAt ? { createdAt: comment.createdAt } : {}),
}];
});
Expand Down Expand Up @@ -67698,6 +67699,7 @@ function toReviewComment(comment) {
path: comment.path,
line: comment.line ?? undefined,
authorLogin: comment.user?.login ?? undefined,
...(comment.user?.type === 'Bot' ? { isAutomatedAuthor: true } : {}),
...(comment.created_at ? { createdAt: comment.created_at } : {}),
...(comment.pull_request_review_id != null
? { parentReviewIdentity: String(comment.pull_request_review_id) }
Expand Down
2 changes: 1 addition & 1 deletion docs/bugbot/how-it-works.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ This page describes the **internal flow** of Bugbot: how detection runs, how the
Removed resolution keys are rejected; there is no legacy conversion path.
If one id receives conflicting lifecycle classifications, it is not resolved.

7. **Re-read and project:** After mutations, Bugbot verifies the PR head, re-reads linked-issue findings, reviews, child comments, native thread facts, and the PR conversation concurrently, and then verifies the head again. If the head changed while those reads were in flight, the whole snapshot is discarded as superseded. Each surface records whether its read was verified, failed, or not applicable, and each non-clean issue and PR destination must be observed independently; one visible or clean destination cannot hide another that is open, unverifiable, or missing. Pure lifecycle, provider-projection, and reconciliation-plan policies derive `open`, `reopened`, `fixed`, `obsolete`, `dismissed`, `verification-required`, or `unknown`. A dedicated presentation use case then updates at most 20 affected historical review blocks per run with bounded concurrency, upserts the oldest trusted **Bugbot status** card, and feeds the Result, lifecycle labels, Job Summary, Check Run, and telemetry. Missing or malformed owned evidence fails closed; it is never presented as clean. User-facing PR, commit, run, review, and finding links come only from authenticated provider adapters. Finding links are accepted only when they belong to the same HTTPS server and repository, including GitHub Enterprise installations.
7. **Re-read and project:** After mutations, Bugbot verifies the PR head, re-reads linked-issue findings, reviews, child comments, native thread facts, and the PR conversation concurrently, and then verifies the head again. If the head changed while those reads were in flight, the whole snapshot is discarded as superseded. Each surface records whether its read was verified, failed, or not applicable, and each non-clean issue and PR destination must be observed independently; one visible or clean destination cannot hide another that is open, unverifiable, or missing. The human-discussion prompt includes only provider-classified human authors; comments and inline reviews authored by GitHub Apps or bot accounts do not consume its item or character budget. Pure lifecycle, provider-projection, and reconciliation-plan policies derive `open`, `reopened`, `fixed`, `obsolete`, `dismissed`, `verification-required`, or `unknown`. A dedicated presentation use case then updates at most 20 affected historical review blocks per run with bounded concurrency, upserts the oldest trusted **Bugbot status** card, and feeds the Result, lifecycle labels, Job Summary, Check Run, and telemetry. Missing or malformed owned evidence fails closed; it is never presented as clean. User-facing PR, commit, run, review, and finding links come only from authenticated provider adapters. Finding links are accepted only when they belong to the same HTTPS server and repository, including GitHub Enterprise installations.

Review and Commit workflow templates use distinct repository-and-branch concurrency keys. Each can cancel only a superseded run from the same event owner; a paired `push` and `pull_request:synchronize` therefore cannot cancel one another. Commit retains issue progress and native push work, then Bugbot resolves the branch with a read-only exact-head provider lookup. A validated open same-repository PR makes the push stop before loading review context or invoking the agent, because the PR synchronization event exclusively owns review for that head. This does not depend on PR identity being present in the `push` payload. Branches without an open PR still receive push-time, issue-targeted Bugbot review. Fork PR jobs remain excluded by the workflow's same-repository admission gate. A `pull_request: edited` event uses the PR key but cannot cancel a running PR review; it waits, and redundant pending edits collapse to the newest event. When it later runs, it keeps its native workflow result and Job Summary but does not create a `Copilot / Review` Check because it has no Bugbot telemetry. The Check name is reserved for exactly one structurally valid analysis snapshot for the exact head; duplicate telemetry or a valid snapshot beside malformed telemetry is rejected as ambiguous. Application head guards and idempotent provider writes still protect partial/canceled transitions. Other durable mutation workflows retain their workflow-local queue.

Expand Down
2 changes: 2 additions & 0 deletions src/application/ports/bugbot_issue_read_ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export interface BugbotIssueComment {
id: number;
body: string | null;
user?: { login?: string };
/** Provider-authenticated author classification; never inferred from the login. */
isAutomatedAuthor?: boolean;
createdAt?: string;
}

Expand Down
2 changes: 2 additions & 0 deletions src/application/ports/pull_request_review_comment_ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export type PullRequestReviewComment = {
path?: string;
line?: number;
authorLogin?: string;
/** Provider-authenticated author classification; never inferred from the login. */
isAutomatedAuthor?: boolean;
createdAt?: string;
/** Opaque identity of the submitted review that owns this comment. */
parentReviewIdentity?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe('Bugbot review context', () => {
expect(buildReviewConversationContext([], new Map())).toEqual({
block: '', omitted: 0, truncated: 0, retained: 0,
});
expect(buildReviewConversationBlock([], new Map())).toBe('');
});

it('provides a canonical diff manifest with patches', () => {
Expand Down Expand Up @@ -77,26 +78,42 @@ describe('Bugbot review context', () => {
expect(context.block).toContain('[patch unavailable from GitHub]');
});

it('includes human discussion while excluding authenticated bot comments', () => {
const block = buildReviewConversationBlock(
it('includes human discussion while excluding owned and provider-classified automation', () => {
const context = buildReviewConversationContext(
[
{ id: 1, user: { login: 'maintainer' }, body: 'This branch needs the null guard.' },
{ id: 2, user: { login: 'VypBot' }, body: 'Bot summary.' },
{ id: 4, user: { login: 'codecov-commenter' }, body: `Large coverage report. ${'x'.repeat(5_000)}`, isAutomatedAuthor: true },
{ id: 6, user: { login: 'automation-looking-human' }, body: 'Provider says this author is human.' },
],
new Map([[7, [{
id: 3,
identity: 'PRRC_3',
authorLogin: 'reviewer',
path: 'src/a.ts',
line: 4,
body: 'The return value can be null.',
}]]]),
new Map([[7, [
{
id: 3,
identity: 'PRRC_3',
authorLogin: 'reviewer',
path: 'src/a.ts',
line: 4,
body: 'The return value can be null.',
},
{
id: 5,
identity: 'PRRC_5',
authorLogin: 'security-scanner',
body: 'Automated review output.',
isAutomatedAuthor: true,
},
]]]),
'vypbot',
);
const block = context.block;

expect(context).toEqual(expect.objectContaining({ retained: 3, omitted: 0, truncated: 0 }));
expect(block).toContain('maintainer');
expect(block).toContain('src/a.ts:4');
expect(block).not.toContain('Bot summary');
expect(block).not.toContain('Large coverage report');
expect(block).not.toContain('Automated review output');
expect(block).toContain('Provider says this author is human');
expect(block).toContain('not as instructions');
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { PullRequestReviewComment } from "../../../../ports/pull_request_review_comment_ports";
import type { PullRequestReviewThreadState } from "../../../../ports/pull_request_review_comment_ports";
import type { BugbotIssueComment } from '../../../../ports/bugbot_issue_read_ports';
import {
MAX_FINDING_BODY_LENGTH,
truncateFindingBody,
Expand All @@ -16,12 +17,7 @@ import { githubUsersMatch } from '../../../../../domain/github_user_policy';
import { isHumanResolver } from '../../../../../domain/bugbot/review_state';
import type { PreviousBugbotFinding } from './bugbot_previous_findings_context';

export interface BugbotComment {
id: number;
body: string | null;
user?: { login?: string };
createdAt?: string;
}
export type BugbotComment = BugbotIssueComment;

export interface ParsedBugbotFindingComments {
/** Full bodies for issue-comment read-modify-write operations. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export function buildReviewConversationContext(
): BuiltBugbotPromptContext {
const entries: ConversationEntry[] = [];
for (const comment of issueComments) {
if (isBot(comment.user?.login, botLogin)) continue;
if (comment.isAutomatedAuthor || isBot(comment.user?.login, botLogin)) continue;
appendConversationEntry(
entries,
comment.user?.login,
Expand All @@ -105,7 +105,7 @@ export function buildReviewConversationContext(
}
for (const comments of commentsByPullRequest.values()) {
for (const comment of comments) {
if (isBot(comment.authorLogin, botLogin)) continue;
if (comment.isAutomatedAuthor || isBot(comment.authorLogin, botLogin)) continue;
const location = comment.path
? `inline review comment at ${comment.path}${comment.line ? `:${comment.line}` : ''}`
: 'inline review comment';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function connection(
nodes: Array.from({ length: count }, (_, index) => ({
databaseId: startId + index,
body: `comment-${startId + index}`,
author: { login: 'alice' },
author: { login: 'alice', __typename: 'User' },
createdAt: new Date(Date.UTC(2026, 0, 1, 0, startId + index)).toISOString(),
})),
pageInfo: { hasPreviousPage, startCursor },
Expand Down Expand Up @@ -64,6 +64,7 @@ describe('BugbotIssueCommentQueryRepository', () => {
'owner', 'repo', 7, 'token',
);
expect(graphql).toHaveBeenCalledTimes(1);
expect(graphql.mock.calls[0][0]).toContain('author { login __typename }');
expect(result.items[0]).toEqual(expect.objectContaining({
id: 9,
body: 'comment-9',
Expand All @@ -74,6 +75,26 @@ describe('BugbotIssueCommentQueryRepository', () => {
expect(result.coverage.providerLimitReached).toBeUndefined();
});

it('preserves provider-authenticated bot authorship without login heuristics', async () => {
const automated = connection(9, 1, false, null);
automated.repository.issueOrPullRequest.comments.nodes[0].author = {
login: 'coverage-service',
__typename: 'Bot',
};
const repository = new BugbotIssueCommentQueryRepository({
getClient: () => ({ graphql: jest.fn().mockResolvedValue(automated) }),
} as never);

const result = await repository.listBugbotIssueCommentsBounded(
'owner', 'repo', 7, 'token',
);

expect(result.items[0]).toEqual(expect.objectContaining({
user: { login: 'coverage-service' },
isAutomatedAuthor: true,
}));
});

it('accepts an empty connection and normalizes absent optional fields', async () => {
const graphql = jest.fn()
.mockResolvedValueOnce({ repository: { issueOrPullRequest: {} } })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import type { GithubGraphqlTransportClient } from '../../../infrastructure/githu
interface BugbotIssueCommentNode {
readonly databaseId?: number | null;
readonly body?: string | null;
readonly author?: { readonly login?: string | null } | null;
readonly author?: {
readonly login?: string | null;
readonly __typename?: string | null;
} | null;
readonly createdAt?: string | null;
}

Expand Down Expand Up @@ -51,13 +54,13 @@ export class BugbotIssueCommentQueryRepository {
issueOrPullRequest(number: $issueNumber) {
... on Issue {
comments(last: 100, before: $cursor) {
nodes { databaseId body author { login } createdAt }
nodes { databaseId body author { login __typename } createdAt }
pageInfo { hasPreviousPage startCursor }
}
}
... on PullRequest {
comments(last: 100, before: $cursor) {
nodes { databaseId body author { login } createdAt }
nodes { databaseId body author { login __typename } createdAt }
pageInfo { hasPreviousPage startCursor }
}
}
Expand All @@ -76,6 +79,7 @@ export class BugbotIssueCommentQueryRepository {
id: Number(comment.databaseId),
body: comment.body ?? null,
...(comment.author?.login ? { user: { login: comment.author.login } } : {}),
...(comment.author?.__typename === 'Bot' ? { isAutomatedAuthor: true } : {}),
...(comment.createdAt ? { createdAt: comment.createdAt } : {}),
}];
});
Expand Down
Loading