From 6778c2281c947130dca7f8ad5444bae691c94a55 Mon Sep 17 00:00:00 2001 From: Pavel Elizarov Date: Mon, 10 Aug 2026 11:51:52 +0400 Subject: [PATCH 1/6] Modified the GitHub action for the new code review task --- src/github/junie/junie-tasks.ts | 10 ++++++++-- src/github/junie/types/junie.ts | 13 +++++++++++++ src/mcp/mcp-prompts.ts | 1 - test/junie-tasks.test.ts | 26 ++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/github/junie/junie-tasks.ts b/src/github/junie/junie-tasks.ts index 075810e1..843d0c46 100644 --- a/src/github/junie/junie-tasks.ts +++ b/src/github/junie/junie-tasks.ts @@ -16,7 +16,7 @@ import {Octokits} from "../api/client"; import {NewGitHubPromptFormatter} from "./new-prompt-formatter"; import {GraphQLGitHubDataFetcher} from "../api/graphql-data-fetcher"; import {FetchedData} from "../api/queries"; -import {CliInput} from "./types/junie"; +import {CliInput, remoteRequestReviewTarget} from "./types/junie"; import {generateMcpToolsPrompt} from "../../mcp/mcp-prompts"; import {junieArgsToString} from "../../utils/junie-args-parser"; @@ -81,9 +81,15 @@ export async function prepareJunieTask( if (isCodeReviewEvent(context)) { const diffPoint = branchInfo.prBaseBranch || branchInfo.baseBranch; const diffCommand = `git diff origin/${diffPoint}...`; + const prNumber = context.entityNumber; + if (!prNumber) { + throw new Error("Code review requires a Pull Request number, but none was found in the event context."); + } junieCLITask.codeReviewTask = { description: promptText, - diffCommand + diffCommand, + fetchVcsInfo: true, + reviewTarget: remoteRequestReviewTarget(prNumber), } } else { junieCLITask.task = promptText; diff --git a/src/github/junie/types/junie.ts b/src/github/junie/types/junie.ts index eb023174..10ed67a5 100644 --- a/src/github/junie/types/junie.ts +++ b/src/github/junie/types/junie.ts @@ -13,9 +13,22 @@ export interface MergeTask { branch: string; } +export interface RemoteRequestReviewTarget { + type: "remoteRequest"; + number: number; +} + +export type ReviewTarget = RemoteRequestReviewTarget; + +export function remoteRequestReviewTarget(prNumber: number): RemoteRequestReviewTarget { + return {type: "remoteRequest", number: prNumber}; +} + export interface CodeReview { description?: string; diffCommand?: string; + fetchVcsInfo?: boolean; + reviewTarget?: ReviewTarget; } export interface CliOutput { diff --git a/src/mcp/mcp-prompts.ts b/src/mcp/mcp-prompts.ts index 2838e480..3a1c1f86 100644 --- a/src/mcp/mcp-prompts.ts +++ b/src/mcp/mcp-prompts.ts @@ -9,7 +9,6 @@ export const MCP_TOOL_PROMPTS = { mcp_github_checks_server: 'Use get_pr_failed_checks_info to retrieve detailed information about failed CI/CD checks if needed.', - mcp_github_inline_comment_server: 'MANDATORY for code reviews: Use post_inline_review_comment to provide inline code review comments. IMPORTANT: If you are responding to a question in an existing review thread (user tagged you in in review thread), DO NOT use this tool - your summary will be automatically posted as a reply in that thread.', youtrack: 'IMPORTANT: Do not post any comments - your summary will be automatically posted by system. And DO NOT update issue status if user did not request it. ALSO: do not look for other issues or any other external information unless explicitly requested by the user.', jira: 'IMPORTANT: Do not post any comments - your summary will be automatically posted by system. And DO NOT update issue status if user did not request it. ALSO: do not look for other issues or any other external information unless explicitly requested by the user.', }; diff --git a/test/junie-tasks.test.ts b/test/junie-tasks.test.ts index dab3e57e..cb1a0ada 100644 --- a/test/junie-tasks.test.ts +++ b/test/junie-tasks.test.ts @@ -423,6 +423,8 @@ describe("prepareJunieTask", () => { expect(result).toBeDefined(); expect(result.codeReviewTask).toBeDefined(); expect(result.codeReviewTask?.diffCommand).toContain("git diff origin/main"); + expect(result.codeReviewTask?.fetchVcsInfo).toBe(true); + expect(result.codeReviewTask?.reviewTarget).toEqual({type: "remoteRequest", number: 123}); expect(result.codeReviewTask?.description).toContain(""); // Header should NOT contain "Your task is to:" expect(result.codeReviewTask?.description).toContain("You were triggered as a GitHub AI Assistant by pull_request action."); @@ -468,6 +470,8 @@ describe("prepareJunieTask", () => { expect(result.codeReviewTask).toBeDefined(); // Should detect code-review trigger from comment and create codeReviewTask expect(result.codeReviewTask?.diffCommand).toContain("git diff origin/main"); + expect(result.codeReviewTask?.fetchVcsInfo).toBe(true); + expect(result.codeReviewTask?.reviewTarget).toEqual({type: "remoteRequest", number: 123}); expect(result.codeReviewTask?.description).toContain(""); // Header should NOT contain "Your task is to:" expect(result.codeReviewTask?.description).toContain("You were triggered as a GitHub AI Assistant by pull_request_review action."); @@ -476,6 +480,28 @@ describe("prepareJunieTask", () => { expect(result.codeReviewTask?.description).not.toContain(""); }); + test("should fail to create codeReviewTask when PR number is not available", async () => { + const context = createMockContext({ + eventName: "workflow_dispatch" as any, + isPR: false, + entityNumber: undefined, + inputs: { + prompt: "code-review" + }, + payload: { + repository: { + owner: {login: "owner"}, + name: "repo" + } + } as any + }); + const octokit = createMockOctokit(); + + await expect(prepareJunieTask(context, branchInfo, octokit)).rejects.toThrow( + "Code review requires a Pull Request number" + ); + }); + test("should not trigger fix CI prompt when workflow_run event has success conclusion", async () => { const context = createMockContext({ eventName: "workflow_run" as any, From 08a5bad8af67c7ba94284359a7e227da264f38d7 Mon Sep 17 00:00:00 2001 From: Pavel Elizarov Date: Tue, 11 Aug 2026 20:36:05 +0400 Subject: [PATCH 2/6] Link up new logic to version --- action.yml | 1 + src/constants/environment.ts | 1 + src/github/context.ts | 2 ++ src/github/junie/junie-tasks.ts | 26 ++++++++++++++++++------ src/github/junie/types/junie.ts | 13 +----------- src/mcp/mcp-prompts.ts | 1 + test/junie-tasks.test.ts | 35 +++++++++++++++++++++++++++++++-- 7 files changed, 59 insertions(+), 20 deletions(-) diff --git a/action.yml b/action.yml index c3adf39a..f56cc3e9 100644 --- a/action.yml +++ b/action.yml @@ -259,6 +259,7 @@ runs: OPENROUTER_API_KEY: ${{ inputs.openrouter_api_key }} GOOGLE_API_KEY: ${{ inputs.google_api_key }} AUTO_COLLECT_FEEDBACK: ${{ inputs.auto_collect_feedback }} + JUNIE_VERSION: ${{ inputs.junie_version }} - name: Install Junie diff --git a/src/constants/environment.ts b/src/constants/environment.ts index 84a0d6c7..55345b0e 100644 --- a/src/constants/environment.ts +++ b/src/constants/environment.ts @@ -30,6 +30,7 @@ export const ENV_VARS = { SKIP_PR: "SKIP_PR", OUTPUT_BRANCH: "OUTPUT_BRANCH", MODEL: "MODEL", + JUNIE_VERSION: "JUNIE_VERSION", // BYOK API keys OPENAI_API_KEY: "OPENAI_API_KEY", diff --git a/src/github/context.ts b/src/github/context.ts index 42d7a392..d786c43e 100644 --- a/src/github/context.ts +++ b/src/github/context.ts @@ -139,6 +139,7 @@ type JunieWorkflowContext = { workingBranch?: string; allowedMcpServers?: string; autoCollectFeedback: boolean; + junieVersion?: string; }; }; @@ -210,6 +211,7 @@ export function extractJunieWorkflowContext(tokenOwner: TokenOwner): JunieExecut targetBranch: process.env.TARGET_BRANCH, allowedMcpServers: process.env.ALLOWED_MCP_SERVERS, autoCollectFeedback: process.env.AUTO_COLLECT_FEEDBACK === "true", + junieVersion: process.env.JUNIE_VERSION, }, }; diff --git a/src/github/junie/junie-tasks.ts b/src/github/junie/junie-tasks.ts index 843d0c46..a9fd3a42 100644 --- a/src/github/junie/junie-tasks.ts +++ b/src/github/junie/junie-tasks.ts @@ -16,10 +16,19 @@ import {Octokits} from "../api/client"; import {NewGitHubPromptFormatter} from "./new-prompt-formatter"; import {GraphQLGitHubDataFetcher} from "../api/graphql-data-fetcher"; import {FetchedData} from "../api/queries"; -import {CliInput, remoteRequestReviewTarget} from "./types/junie"; +import {CliInput} from "./types/junie"; import {generateMcpToolsPrompt} from "../../mcp/mcp-prompts"; import {junieArgsToString} from "../../utils/junie-args-parser"; +// First Junie build that understands `reviewTarget` in the code review task input. +const MIN_BUILD_WITH_REVIEW_TARGET = 4000.1; // TODO: change to the real version + +function supportsReviewTarget(junieVersion: string | undefined): boolean { + if (junieVersion === "latest") return true; + const build = Number(junieVersion?.split(".")[0]); + return Number.isFinite(build) && build >= MIN_BUILD_WITH_REVIEW_TARGET; +} + function getTriggerTime(context: JunieExecutionContext): string | undefined { if (isIssueCommentEvent(context)) { return context.payload.comment.created_at; @@ -71,8 +80,13 @@ export async function prepareJunieTask( console.log(`Extracted custom junie args: ${customJunieArgs.join(' ')}`); } - // Append MCP tools information if any MCP servers are enabled - const mcpToolsPrompt = generateMcpToolsPrompt(enabledMcpServers); + // Append MCP tools information if any MCP servers are enabled. + // Junie posts review comments itself via `reviewTarget`, so the inline comment tool is not advertised for code review. + const useReviewTarget = isCodeReviewEvent(context) && supportsReviewTarget(context.inputs.junieVersion); + const mcpServersForPrompt = useReviewTarget + ? enabledMcpServers.filter(server => server !== "mcp_github_inline_comment_server") + : enabledMcpServers; + const mcpToolsPrompt = generateMcpToolsPrompt(mcpServersForPrompt); if (mcpToolsPrompt) { promptText = promptText + mcpToolsPrompt; } @@ -81,15 +95,15 @@ export async function prepareJunieTask( if (isCodeReviewEvent(context)) { const diffPoint = branchInfo.prBaseBranch || branchInfo.baseBranch; const diffCommand = `git diff origin/${diffPoint}...`; - const prNumber = context.entityNumber; - if (!prNumber) { + const prNumber = context.isPR ? context.entityNumber : undefined; + if (useReviewTarget && !prNumber) { throw new Error("Code review requires a Pull Request number, but none was found in the event context."); } junieCLITask.codeReviewTask = { description: promptText, diffCommand, fetchVcsInfo: true, - reviewTarget: remoteRequestReviewTarget(prNumber), + ...(useReviewTarget ? {reviewTarget: {type: "remoteRequest" as const, number: prNumber!}} : {}), } } else { junieCLITask.task = promptText; diff --git a/src/github/junie/types/junie.ts b/src/github/junie/types/junie.ts index 10ed67a5..aa1d8fca 100644 --- a/src/github/junie/types/junie.ts +++ b/src/github/junie/types/junie.ts @@ -13,22 +13,11 @@ export interface MergeTask { branch: string; } -export interface RemoteRequestReviewTarget { - type: "remoteRequest"; - number: number; -} - -export type ReviewTarget = RemoteRequestReviewTarget; - -export function remoteRequestReviewTarget(prNumber: number): RemoteRequestReviewTarget { - return {type: "remoteRequest", number: prNumber}; -} - export interface CodeReview { description?: string; diffCommand?: string; fetchVcsInfo?: boolean; - reviewTarget?: ReviewTarget; + reviewTarget?: { type: "remoteRequest"; number: number }; } export interface CliOutput { diff --git a/src/mcp/mcp-prompts.ts b/src/mcp/mcp-prompts.ts index 3a1c1f86..2838e480 100644 --- a/src/mcp/mcp-prompts.ts +++ b/src/mcp/mcp-prompts.ts @@ -9,6 +9,7 @@ export const MCP_TOOL_PROMPTS = { mcp_github_checks_server: 'Use get_pr_failed_checks_info to retrieve detailed information about failed CI/CD checks if needed.', + mcp_github_inline_comment_server: 'MANDATORY for code reviews: Use post_inline_review_comment to provide inline code review comments. IMPORTANT: If you are responding to a question in an existing review thread (user tagged you in in review thread), DO NOT use this tool - your summary will be automatically posted as a reply in that thread.', youtrack: 'IMPORTANT: Do not post any comments - your summary will be automatically posted by system. And DO NOT update issue status if user did not request it. ALSO: do not look for other issues or any other external information unless explicitly requested by the user.', jira: 'IMPORTANT: Do not post any comments - your summary will be automatically posted by system. And DO NOT update issue status if user did not request it. ALSO: do not look for other issues or any other external information unless explicitly requested by the user.', }; diff --git a/test/junie-tasks.test.ts b/test/junie-tasks.test.ts index cb1a0ada..b0a94935 100644 --- a/test/junie-tasks.test.ts +++ b/test/junie-tasks.test.ts @@ -29,7 +29,8 @@ describe("prepareJunieTask", () => { triggerPhrase: "@junie-agent", assigneeTrigger: "", labelTrigger: "", - allowedMcpServers: "" + allowedMcpServers: "", + junieVersion: "latest" }; const { inputs: _, ...restOverrides } = overrides; @@ -418,13 +419,14 @@ describe("prepareJunieTask", () => { }); const octokit = createMockOctokit(); - const result = await prepareJunieTask(context, branchInfo, octokit); + const result = await prepareJunieTask(context, branchInfo, octokit, ["mcp_github_inline_comment_server"]); expect(result).toBeDefined(); expect(result.codeReviewTask).toBeDefined(); expect(result.codeReviewTask?.diffCommand).toContain("git diff origin/main"); expect(result.codeReviewTask?.fetchVcsInfo).toBe(true); expect(result.codeReviewTask?.reviewTarget).toEqual({type: "remoteRequest", number: 123}); + expect(result.codeReviewTask?.description).not.toContain("post_inline_review_comment"); expect(result.codeReviewTask?.description).toContain(""); // Header should NOT contain "Your task is to:" expect(result.codeReviewTask?.description).toContain("You were triggered as a GitHub AI Assistant by pull_request action."); @@ -434,6 +436,35 @@ describe("prepareJunieTask", () => { expect(result.codeReviewTask?.description).not.toContain("code-review"); }); + test("should not use reviewTarget on Junie versions without support", async () => { + const context = createMockContext({ + eventName: "pull_request", + isPR: true, + entityNumber: 123, + inputs: { + prompt: "code-review", + junieVersion: "2698.3" + }, + payload: { + pull_request: { + number: 123, + title: "Test PR", + updated_at: "2024-01-01T00:00:00Z" + }, + repository: { + owner: {login: "owner"}, + name: "repo" + } + } as any + }); + const octokit = createMockOctokit(); + + const result = await prepareJunieTask(context, branchInfo, octokit, ["mcp_github_inline_comment_server"]); + + expect(result.codeReviewTask?.reviewTarget).toBeUndefined(); + expect(result.codeReviewTask?.description).toContain("post_inline_review_comment"); + }); + test("should trigger codeReviewTask from comment when inputs.prompt is empty and code-review keyword is used", async () => { const context = createMockContext({ eventName: "pull_request_review", From b66c90d6cb84258fa12a1519473c29f004a12a67 Mon Sep 17 00:00:00 2001 From: Pavel Elizarov Date: Fri, 14 Aug 2026 13:37:08 +0100 Subject: [PATCH 3/6] Action update --- action.yml | 1 - src/constants/environment.ts | 1 - src/github/context.ts | 2 -- src/github/junie/junie-tasks.ts | 26 ++++++------------------ src/github/junie/types/junie.ts | 13 +++++++++++- src/mcp/mcp-prompts.ts | 1 - test/junie-tasks.test.ts | 35 ++------------------------------- 7 files changed, 20 insertions(+), 59 deletions(-) diff --git a/action.yml b/action.yml index f56cc3e9..c3adf39a 100644 --- a/action.yml +++ b/action.yml @@ -259,7 +259,6 @@ runs: OPENROUTER_API_KEY: ${{ inputs.openrouter_api_key }} GOOGLE_API_KEY: ${{ inputs.google_api_key }} AUTO_COLLECT_FEEDBACK: ${{ inputs.auto_collect_feedback }} - JUNIE_VERSION: ${{ inputs.junie_version }} - name: Install Junie diff --git a/src/constants/environment.ts b/src/constants/environment.ts index 55345b0e..84a0d6c7 100644 --- a/src/constants/environment.ts +++ b/src/constants/environment.ts @@ -30,7 +30,6 @@ export const ENV_VARS = { SKIP_PR: "SKIP_PR", OUTPUT_BRANCH: "OUTPUT_BRANCH", MODEL: "MODEL", - JUNIE_VERSION: "JUNIE_VERSION", // BYOK API keys OPENAI_API_KEY: "OPENAI_API_KEY", diff --git a/src/github/context.ts b/src/github/context.ts index d786c43e..42d7a392 100644 --- a/src/github/context.ts +++ b/src/github/context.ts @@ -139,7 +139,6 @@ type JunieWorkflowContext = { workingBranch?: string; allowedMcpServers?: string; autoCollectFeedback: boolean; - junieVersion?: string; }; }; @@ -211,7 +210,6 @@ export function extractJunieWorkflowContext(tokenOwner: TokenOwner): JunieExecut targetBranch: process.env.TARGET_BRANCH, allowedMcpServers: process.env.ALLOWED_MCP_SERVERS, autoCollectFeedback: process.env.AUTO_COLLECT_FEEDBACK === "true", - junieVersion: process.env.JUNIE_VERSION, }, }; diff --git a/src/github/junie/junie-tasks.ts b/src/github/junie/junie-tasks.ts index a9fd3a42..843d0c46 100644 --- a/src/github/junie/junie-tasks.ts +++ b/src/github/junie/junie-tasks.ts @@ -16,19 +16,10 @@ import {Octokits} from "../api/client"; import {NewGitHubPromptFormatter} from "./new-prompt-formatter"; import {GraphQLGitHubDataFetcher} from "../api/graphql-data-fetcher"; import {FetchedData} from "../api/queries"; -import {CliInput} from "./types/junie"; +import {CliInput, remoteRequestReviewTarget} from "./types/junie"; import {generateMcpToolsPrompt} from "../../mcp/mcp-prompts"; import {junieArgsToString} from "../../utils/junie-args-parser"; -// First Junie build that understands `reviewTarget` in the code review task input. -const MIN_BUILD_WITH_REVIEW_TARGET = 4000.1; // TODO: change to the real version - -function supportsReviewTarget(junieVersion: string | undefined): boolean { - if (junieVersion === "latest") return true; - const build = Number(junieVersion?.split(".")[0]); - return Number.isFinite(build) && build >= MIN_BUILD_WITH_REVIEW_TARGET; -} - function getTriggerTime(context: JunieExecutionContext): string | undefined { if (isIssueCommentEvent(context)) { return context.payload.comment.created_at; @@ -80,13 +71,8 @@ export async function prepareJunieTask( console.log(`Extracted custom junie args: ${customJunieArgs.join(' ')}`); } - // Append MCP tools information if any MCP servers are enabled. - // Junie posts review comments itself via `reviewTarget`, so the inline comment tool is not advertised for code review. - const useReviewTarget = isCodeReviewEvent(context) && supportsReviewTarget(context.inputs.junieVersion); - const mcpServersForPrompt = useReviewTarget - ? enabledMcpServers.filter(server => server !== "mcp_github_inline_comment_server") - : enabledMcpServers; - const mcpToolsPrompt = generateMcpToolsPrompt(mcpServersForPrompt); + // Append MCP tools information if any MCP servers are enabled + const mcpToolsPrompt = generateMcpToolsPrompt(enabledMcpServers); if (mcpToolsPrompt) { promptText = promptText + mcpToolsPrompt; } @@ -95,15 +81,15 @@ export async function prepareJunieTask( if (isCodeReviewEvent(context)) { const diffPoint = branchInfo.prBaseBranch || branchInfo.baseBranch; const diffCommand = `git diff origin/${diffPoint}...`; - const prNumber = context.isPR ? context.entityNumber : undefined; - if (useReviewTarget && !prNumber) { + const prNumber = context.entityNumber; + if (!prNumber) { throw new Error("Code review requires a Pull Request number, but none was found in the event context."); } junieCLITask.codeReviewTask = { description: promptText, diffCommand, fetchVcsInfo: true, - ...(useReviewTarget ? {reviewTarget: {type: "remoteRequest" as const, number: prNumber!}} : {}), + reviewTarget: remoteRequestReviewTarget(prNumber), } } else { junieCLITask.task = promptText; diff --git a/src/github/junie/types/junie.ts b/src/github/junie/types/junie.ts index aa1d8fca..10ed67a5 100644 --- a/src/github/junie/types/junie.ts +++ b/src/github/junie/types/junie.ts @@ -13,11 +13,22 @@ export interface MergeTask { branch: string; } +export interface RemoteRequestReviewTarget { + type: "remoteRequest"; + number: number; +} + +export type ReviewTarget = RemoteRequestReviewTarget; + +export function remoteRequestReviewTarget(prNumber: number): RemoteRequestReviewTarget { + return {type: "remoteRequest", number: prNumber}; +} + export interface CodeReview { description?: string; diffCommand?: string; fetchVcsInfo?: boolean; - reviewTarget?: { type: "remoteRequest"; number: number }; + reviewTarget?: ReviewTarget; } export interface CliOutput { diff --git a/src/mcp/mcp-prompts.ts b/src/mcp/mcp-prompts.ts index 2838e480..3a1c1f86 100644 --- a/src/mcp/mcp-prompts.ts +++ b/src/mcp/mcp-prompts.ts @@ -9,7 +9,6 @@ export const MCP_TOOL_PROMPTS = { mcp_github_checks_server: 'Use get_pr_failed_checks_info to retrieve detailed information about failed CI/CD checks if needed.', - mcp_github_inline_comment_server: 'MANDATORY for code reviews: Use post_inline_review_comment to provide inline code review comments. IMPORTANT: If you are responding to a question in an existing review thread (user tagged you in in review thread), DO NOT use this tool - your summary will be automatically posted as a reply in that thread.', youtrack: 'IMPORTANT: Do not post any comments - your summary will be automatically posted by system. And DO NOT update issue status if user did not request it. ALSO: do not look for other issues or any other external information unless explicitly requested by the user.', jira: 'IMPORTANT: Do not post any comments - your summary will be automatically posted by system. And DO NOT update issue status if user did not request it. ALSO: do not look for other issues or any other external information unless explicitly requested by the user.', }; diff --git a/test/junie-tasks.test.ts b/test/junie-tasks.test.ts index b0a94935..cb1a0ada 100644 --- a/test/junie-tasks.test.ts +++ b/test/junie-tasks.test.ts @@ -29,8 +29,7 @@ describe("prepareJunieTask", () => { triggerPhrase: "@junie-agent", assigneeTrigger: "", labelTrigger: "", - allowedMcpServers: "", - junieVersion: "latest" + allowedMcpServers: "" }; const { inputs: _, ...restOverrides } = overrides; @@ -419,14 +418,13 @@ describe("prepareJunieTask", () => { }); const octokit = createMockOctokit(); - const result = await prepareJunieTask(context, branchInfo, octokit, ["mcp_github_inline_comment_server"]); + const result = await prepareJunieTask(context, branchInfo, octokit); expect(result).toBeDefined(); expect(result.codeReviewTask).toBeDefined(); expect(result.codeReviewTask?.diffCommand).toContain("git diff origin/main"); expect(result.codeReviewTask?.fetchVcsInfo).toBe(true); expect(result.codeReviewTask?.reviewTarget).toEqual({type: "remoteRequest", number: 123}); - expect(result.codeReviewTask?.description).not.toContain("post_inline_review_comment"); expect(result.codeReviewTask?.description).toContain(""); // Header should NOT contain "Your task is to:" expect(result.codeReviewTask?.description).toContain("You were triggered as a GitHub AI Assistant by pull_request action."); @@ -436,35 +434,6 @@ describe("prepareJunieTask", () => { expect(result.codeReviewTask?.description).not.toContain("code-review"); }); - test("should not use reviewTarget on Junie versions without support", async () => { - const context = createMockContext({ - eventName: "pull_request", - isPR: true, - entityNumber: 123, - inputs: { - prompt: "code-review", - junieVersion: "2698.3" - }, - payload: { - pull_request: { - number: 123, - title: "Test PR", - updated_at: "2024-01-01T00:00:00Z" - }, - repository: { - owner: {login: "owner"}, - name: "repo" - } - } as any - }); - const octokit = createMockOctokit(); - - const result = await prepareJunieTask(context, branchInfo, octokit, ["mcp_github_inline_comment_server"]); - - expect(result.codeReviewTask?.reviewTarget).toBeUndefined(); - expect(result.codeReviewTask?.description).toContain("post_inline_review_comment"); - }); - test("should trigger codeReviewTask from comment when inputs.prompt is empty and code-review keyword is used", async () => { const context = createMockContext({ eventName: "pull_request_review", From 0b41a473b8fb04514149c221e3ebbab2420fef1f Mon Sep 17 00:00:00 2001 From: Pavel Elizarov Date: Tue, 1 Sep 2026 13:10:31 +0400 Subject: [PATCH 4/6] Updated tests --- test/junie-tasks.test.ts | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/test/junie-tasks.test.ts b/test/junie-tasks.test.ts index cb1a0ada..2e6ad80e 100644 --- a/test/junie-tasks.test.ts +++ b/test/junie-tasks.test.ts @@ -397,16 +397,19 @@ describe("prepareJunieTask", () => { }); test("should create codeReviewTask when code-review prompt is provided", async () => { + // Deliberately different from the default entity number: the review target + // must carry the number of the reviewed PR, not a hardcoded value + const prNumber = 4242; const context = createMockContext({ eventName: "pull_request", isPR: true, - entityNumber: 123, + entityNumber: prNumber, inputs: { prompt: "code-review" }, payload: { pull_request: { - number: 123, + number: prNumber, title: "Test PR", updated_at: "2024-01-01T00:00:00Z" }, @@ -418,13 +421,25 @@ describe("prepareJunieTask", () => { }); const octokit = createMockOctokit(); - const result = await prepareJunieTask(context, branchInfo, octokit); + // Inline comments are posted by the CLI itself, so the inline comment MCP server + // must not be advertised in the prompt anymore even when it is enabled + const result = await prepareJunieTask(context, branchInfo, octokit, ["mcp_github_inline_comment_server"]); expect(result).toBeDefined(); + expect(result.task).toBeUndefined(); + expect(result.mergeTask).toBeUndefined(); expect(result.codeReviewTask).toBeDefined(); + // The Junie CLI parses the whole input strictly and rejects it on unknown fields, + // so the exact set of keys is part of the contract, not an implementation detail + expect(Object.keys(result.codeReviewTask!).sort()).toEqual([ + "description", + "diffCommand", + "fetchVcsInfo", + "reviewTarget" + ]); expect(result.codeReviewTask?.diffCommand).toContain("git diff origin/main"); expect(result.codeReviewTask?.fetchVcsInfo).toBe(true); - expect(result.codeReviewTask?.reviewTarget).toEqual({type: "remoteRequest", number: 123}); + expect(result.codeReviewTask?.reviewTarget).toEqual({type: "remoteRequest", number: prNumber}); expect(result.codeReviewTask?.description).toContain(""); // Header should NOT contain "Your task is to:" expect(result.codeReviewTask?.description).toContain("You were triggered as a GitHub AI Assistant by pull_request action."); @@ -432,6 +447,12 @@ describe("prepareJunieTask", () => { // For code review, user_instruction should not be attached at all expect(result.codeReviewTask?.description).not.toContain(""); expect(result.codeReviewTask?.description).not.toContain("code-review"); + expect(result.codeReviewTask?.description).not.toContain("post_inline_review_comment"); + + // The CLI reads the task from the file, not from the returned object + const junieInputFile = `${process.env.WORKING_DIR}/junie_input.json`; + expect(core.setOutput).toHaveBeenCalledWith("JUNIE_INPUT_FILE", junieInputFile); + expect(JSON.parse(fs.readFileSync(junieInputFile, "utf-8"))).toEqual(result); }); test("should trigger codeReviewTask from comment when inputs.prompt is empty and code-review keyword is used", async () => { @@ -467,8 +488,15 @@ describe("prepareJunieTask", () => { const result = await prepareJunieTask(context, branchInfo, octokit); expect(result).toBeDefined(); + expect(result.task).toBeUndefined(); expect(result.codeReviewTask).toBeDefined(); // Should detect code-review trigger from comment and create codeReviewTask + expect(Object.keys(result.codeReviewTask!).sort()).toEqual([ + "description", + "diffCommand", + "fetchVcsInfo", + "reviewTarget" + ]); expect(result.codeReviewTask?.diffCommand).toContain("git diff origin/main"); expect(result.codeReviewTask?.fetchVcsInfo).toBe(true); expect(result.codeReviewTask?.reviewTarget).toEqual({type: "remoteRequest", number: 123}); From 19c3cf8edaad43ea1d439cc22d4bcb11603ca204 Mon Sep 17 00:00:00 2001 From: Pavel Elizarov Date: Tue, 1 Sep 2026 15:16:47 +0400 Subject: [PATCH 5/6] Updated tests #2 --- .github/workflows/e2e-tests-workflow.yml | 15 +++- test/junie-tasks.test.ts | 96 ++++++++++++++++++++---- 2 files changed, 94 insertions(+), 17 deletions(-) diff --git a/.github/workflows/e2e-tests-workflow.yml b/.github/workflows/e2e-tests-workflow.yml index 52a33797..9270647b 100644 --- a/.github/workflows/e2e-tests-workflow.yml +++ b/.github/workflows/e2e-tests-workflow.yml @@ -45,8 +45,19 @@ jobs: exit_code=0 for test_file in test/integration/*.test.ts; do test_name=$(basename "$test_file" .test.ts) - echo "Starting $test_file" - bun test "$test_file" 2>&1 | tee "test-results/$test_name.log" & + + # Code review passes codeReviewTask.reviewTarget to the CLI. Released builds + # reject the whole input with "Cannot parse input JSON" (first build with + # reviewTarget support: 3056.1), so these tests need the nightly channel. + # Drop this override once reviewTarget ships in a release and the + # junie_version default in action.yml is bumped to it. + junie_version="" + if [ "$test_name" = "pr_code_review" ]; then + junie_version="latest" + fi + + echo "Starting $test_file${junie_version:+ with Junie version $junie_version}" + JUNIE_VERSION="$junie_version" bun test "$test_file" 2>&1 | tee "test-results/$test_name.log" & done for job in $(jobs -p); do diff --git a/test/junie-tasks.test.ts b/test/junie-tasks.test.ts index 2e6ad80e..471f6ef1 100644 --- a/test/junie-tasks.test.ts +++ b/test/junie-tasks.test.ts @@ -15,6 +15,70 @@ mock.module("../src/github/junie/attachment-downloader", () => ({ downloadAttachmentsAndRewriteText: mock((text: string) => Promise.resolve(text)), })); +/** + * The input schema the Junie CLI accepts, mirrored from the CLI sources: `CliInput` and + * `CliCodeReviewTask` (api/Input.kt) and `ReviewTarget` (attachments/ReviewTarget.kt). + * The CLI decodes the input strictly, so a single unknown key or an unknown `reviewTarget.type` + * aborts the whole run with "Cannot parse input JSON" before any task starts. + */ +const CLI_INPUT_KEYS = [ + "codeReviewTask", + "debugTask", + "mergeTask", + "orchestratedTask", + "rebaseTask", + "sessionId", + "task" +]; + +const CODE_REVIEW_TASK_KEYS = [ + "description", + "diffCommand", + "fetchVcsInfo", + // Deprecated on the CLI side: superseded by reviewTarget, which also selects the comment + // channel. Sending it switches the review back to the external MCP comment tool. + "includeInlineCommentToolInstructions", + "reviewTarget" +]; + +const REVIEW_TARGET_TYPES = ["localChanges", "remoteRequest"]; + +/** The keys the action is expected to send for a code review, in the order `sort()` produces. */ +const EXPECTED_CODE_REVIEW_TASK_KEYS = ["description", "diffCommand", "fetchVcsInfo", "reviewTarget"]; + +/** Asserts the payload carries nothing the CLI's strict parser would reject. */ +const expectParseableByJunieCli = (input: Record) => { + for (const key of Object.keys(input)) { + expect(CLI_INPUT_KEYS).toContain(key); + } + + if (input.mergeTask) { + expect(Object.keys(input.mergeTask)).toEqual(["branch"]); + expect(typeof input.mergeTask.branch).toBe("string"); + } + + const codeReviewTask = input.codeReviewTask; + if (codeReviewTask) { + for (const key of Object.keys(codeReviewTask)) { + expect(CODE_REVIEW_TASK_KEYS).toContain(key); + } + + const reviewTarget = codeReviewTask.reviewTarget; + if (reviewTarget) { + expect(REVIEW_TARGET_TYPES).toContain(reviewTarget.type); + if (reviewTarget.type === "remoteRequest") { + expect(Object.keys(reviewTarget).sort()).toEqual(["number", "type"]); + // Parsed into a Kotlin Int, so a float or a stringified number fails the parser + expect(Number.isInteger(reviewTarget.number)).toBe(true); + } + } + } +}; + +/** Reads back the file the CLI is actually fed, instead of the object `prepareJunieTask` returns. */ +const readJunieInputFile = (): Record => + JSON.parse(fs.readFileSync(`${process.env.WORKING_DIR}/junie_input.json`, "utf-8")); + describe("prepareJunieTask", () => { const createMockContext = (overrides: Partial = {}): JunieExecutionContext => { const defaultInputs = { @@ -431,12 +495,11 @@ describe("prepareJunieTask", () => { expect(result.codeReviewTask).toBeDefined(); // The Junie CLI parses the whole input strictly and rejects it on unknown fields, // so the exact set of keys is part of the contract, not an implementation detail - expect(Object.keys(result.codeReviewTask!).sort()).toEqual([ - "description", - "diffCommand", - "fetchVcsInfo", - "reviewTarget" - ]); + expect(Object.keys(result.codeReviewTask!).sort()).toEqual(EXPECTED_CODE_REVIEW_TASK_KEYS); + // reviewTarget alone must select the comment channel: the deprecated flag would + // pin the review to the external MCP comment tool instead of the CLI's own one + expect(result.codeReviewTask).not.toHaveProperty("includeInlineCommentToolInstructions"); + // The base ref the CLI derives from the command must stay resolvable expect(result.codeReviewTask?.diffCommand).toContain("git diff origin/main"); expect(result.codeReviewTask?.fetchVcsInfo).toBe(true); expect(result.codeReviewTask?.reviewTarget).toEqual({type: "remoteRequest", number: prNumber}); @@ -450,9 +513,10 @@ describe("prepareJunieTask", () => { expect(result.codeReviewTask?.description).not.toContain("post_inline_review_comment"); // The CLI reads the task from the file, not from the returned object - const junieInputFile = `${process.env.WORKING_DIR}/junie_input.json`; - expect(core.setOutput).toHaveBeenCalledWith("JUNIE_INPUT_FILE", junieInputFile); - expect(JSON.parse(fs.readFileSync(junieInputFile, "utf-8"))).toEqual(result); + expect(core.setOutput).toHaveBeenCalledWith("JUNIE_INPUT_FILE", `${process.env.WORKING_DIR}/junie_input.json`); + const writtenInput = readJunieInputFile(); + expect(writtenInput).toEqual(result); + expectParseableByJunieCli(writtenInput); }); test("should trigger codeReviewTask from comment when inputs.prompt is empty and code-review keyword is used", async () => { @@ -491,12 +555,7 @@ describe("prepareJunieTask", () => { expect(result.task).toBeUndefined(); expect(result.codeReviewTask).toBeDefined(); // Should detect code-review trigger from comment and create codeReviewTask - expect(Object.keys(result.codeReviewTask!).sort()).toEqual([ - "description", - "diffCommand", - "fetchVcsInfo", - "reviewTarget" - ]); + expect(Object.keys(result.codeReviewTask!).sort()).toEqual(EXPECTED_CODE_REVIEW_TASK_KEYS); expect(result.codeReviewTask?.diffCommand).toContain("git diff origin/main"); expect(result.codeReviewTask?.fetchVcsInfo).toBe(true); expect(result.codeReviewTask?.reviewTarget).toEqual({type: "remoteRequest", number: 123}); @@ -506,6 +565,10 @@ describe("prepareJunieTask", () => { expect(result.codeReviewTask?.description).not.toContain("Your task is to:"); // For code review, user_instruction should not be attached expect(result.codeReviewTask?.description).not.toContain(""); + + const writtenInput = readJunieInputFile(); + expect(writtenInput).toEqual(result); + expectParseableByJunieCli(writtenInput); }); test("should fail to create codeReviewTask when PR number is not available", async () => { @@ -650,6 +713,7 @@ describe("prepareJunieTask", () => { expect(result.mergeTask).toBeDefined(); expect(result.task).toBeUndefined(); expect(result.mergeTask?.branch).toBe("main"); + expectParseableByJunieCli(readJunieInputFile()); }); test("should set merge task when comment has resolve trigger phrase", async () => { @@ -682,6 +746,7 @@ describe("prepareJunieTask", () => { expect(result.mergeTask).toBeDefined(); expect(result.task).toBeUndefined(); expect(result.mergeTask?.branch).toBe("main"); + expectParseableByJunieCli(readJunieInputFile()); }); }); @@ -707,6 +772,7 @@ describe("prepareJunieTask", () => { expect(result).toBeDefined(); expect(core.setOutput).toHaveBeenCalledWith("JUNIE_INPUT_FILE", expect.any(String)); expect(core.setOutput).toHaveBeenCalledWith("CUSTOM_JUNIE_ARGS", expect.any(String)); + expectParseableByJunieCli(readJunieInputFile()); }); }); From b8d6dffb020c6cbcbc51a629c597f7b3a0edd12c Mon Sep 17 00:00:00 2001 From: Pavel Elizarov Date: Wed, 2 Sep 2026 15:32:17 +0400 Subject: [PATCH 6/6] Updated tests --- .github/workflows/e2e-tests-workflow.yml | 15 ++------- action.yml | 1 + src/constants/environment.ts | 1 + src/github/context.ts | 2 ++ src/github/junie/junie-tasks.ts | 21 ++++++++++--- src/mcp/mcp-prompts.ts | 3 ++ test/junie-tasks.test.ts | 39 +++++++++++++++++++++++- 7 files changed, 64 insertions(+), 18 deletions(-) diff --git a/.github/workflows/e2e-tests-workflow.yml b/.github/workflows/e2e-tests-workflow.yml index 9270647b..52a33797 100644 --- a/.github/workflows/e2e-tests-workflow.yml +++ b/.github/workflows/e2e-tests-workflow.yml @@ -45,19 +45,8 @@ jobs: exit_code=0 for test_file in test/integration/*.test.ts; do test_name=$(basename "$test_file" .test.ts) - - # Code review passes codeReviewTask.reviewTarget to the CLI. Released builds - # reject the whole input with "Cannot parse input JSON" (first build with - # reviewTarget support: 3056.1), so these tests need the nightly channel. - # Drop this override once reviewTarget ships in a release and the - # junie_version default in action.yml is bumped to it. - junie_version="" - if [ "$test_name" = "pr_code_review" ]; then - junie_version="latest" - fi - - echo "Starting $test_file${junie_version:+ with Junie version $junie_version}" - JUNIE_VERSION="$junie_version" bun test "$test_file" 2>&1 | tee "test-results/$test_name.log" & + echo "Starting $test_file" + bun test "$test_file" 2>&1 | tee "test-results/$test_name.log" & done for job in $(jobs -p); do diff --git a/action.yml b/action.yml index 2a36dc85..3cbacca9 100644 --- a/action.yml +++ b/action.yml @@ -259,6 +259,7 @@ runs: OPENROUTER_API_KEY: ${{ inputs.openrouter_api_key }} GOOGLE_API_KEY: ${{ inputs.google_api_key }} AUTO_COLLECT_FEEDBACK: ${{ inputs.auto_collect_feedback }} + JUNIE_VERSION: ${{ inputs.junie_version }} - name: Install Junie diff --git a/src/constants/environment.ts b/src/constants/environment.ts index 84a0d6c7..55e8b641 100644 --- a/src/constants/environment.ts +++ b/src/constants/environment.ts @@ -26,6 +26,7 @@ export const ENV_VARS = { RESOLVE_CONFLICTS: "RESOLVE_CONFLICTS", CREATE_NEW_BRANCH_FOR_PR: "CREATE_NEW_BRANCH_FOR_PR", SILENT_MODE: "SILENT_MODE", + JUNIE_VERSION: "JUNIE_VERSION", SKIP_REVIEW_SUMMARY: "SKIP_REVIEW_SUMMARY", SKIP_PR: "SKIP_PR", OUTPUT_BRANCH: "OUTPUT_BRANCH", diff --git a/src/github/context.ts b/src/github/context.ts index 42d7a392..8e7f3d73 100644 --- a/src/github/context.ts +++ b/src/github/context.ts @@ -139,6 +139,7 @@ type JunieWorkflowContext = { workingBranch?: string; allowedMcpServers?: string; autoCollectFeedback: boolean; + junieVersion: string; }; }; @@ -210,6 +211,7 @@ export function extractJunieWorkflowContext(tokenOwner: TokenOwner): JunieExecut targetBranch: process.env.TARGET_BRANCH, allowedMcpServers: process.env.ALLOWED_MCP_SERVERS, autoCollectFeedback: process.env.AUTO_COLLECT_FEEDBACK === "true", + junieVersion: process.env.JUNIE_VERSION ?? "", }, }; diff --git a/src/github/junie/junie-tasks.ts b/src/github/junie/junie-tasks.ts index 68115853..09d7f722 100644 --- a/src/github/junie/junie-tasks.ts +++ b/src/github/junie/junie-tasks.ts @@ -17,10 +17,22 @@ import {NewGitHubPromptFormatter} from "./new-prompt-formatter"; import {GraphQLGitHubDataFetcher} from "../api/graphql-data-fetcher"; import {FetchedData} from "../api/queries"; import {CliInput, remoteRequestReviewTarget} from "./types/junie"; -import {generateMcpToolsPrompt} from "../../mcp/mcp-prompts"; +import {generateMcpToolsPrompt, LEGACY_INLINE_COMMENT_TOOL_PROMPT} from "../../mcp/mcp-prompts"; import {junieArgsToString} from "../../utils/junie-args-parser"; import {buildDiffCommand} from "../../constants/github"; +/** + * First Junie CLI build accepting `codeReviewTask.reviewTarget`. Older builds parse the input + * strictly and abort the run with "Cannot parse input JSON" on an unknown field, so they get + * the payload they shipped with and post inline comments through the MCP server instead. + */ +const MIN_VERSION_WITH_REVIEW_TARGET = 3056.1; + +/** `latest` installs the nightly channel; an unparseable version is assumed to be old. */ +function supportsReviewTarget(version: string): boolean { + return version.trim() === "latest" || parseFloat(version) >= MIN_VERSION_WITH_REVIEW_TARGET; +} + function getTriggerTime(context: JunieExecutionContext): string | undefined { if (isIssueCommentEvent(context)) { return context.payload.comment.created_at; @@ -86,11 +98,12 @@ export async function prepareJunieTask( throw new Error("Code review requires a Pull Request number, but none was found in the event context."); } const diffCommand = buildDiffCommand(diffPoint, branchInfo.mergeBaseSha); + const legacyCli = !supportsReviewTarget(context.inputs.junieVersion); + const legacyInlineComments = legacyCli && enabledMcpServers.includes("mcp_github_inline_comment_server"); junieCLITask.codeReviewTask = { - description: promptText, + description: legacyInlineComments ? `${promptText}\n\n${LEGACY_INLINE_COMMENT_TOOL_PROMPT}` : promptText, diffCommand, - fetchVcsInfo: true, - reviewTarget: remoteRequestReviewTarget(prNumber), + ...(legacyCli ? {} : {fetchVcsInfo: true, reviewTarget: remoteRequestReviewTarget(prNumber)}), } } else { junieCLITask.task = promptText; diff --git a/src/mcp/mcp-prompts.ts b/src/mcp/mcp-prompts.ts index 3a1c1f86..497e368c 100644 --- a/src/mcp/mcp-prompts.ts +++ b/src/mcp/mcp-prompts.ts @@ -13,6 +13,9 @@ export const MCP_TOOL_PROMPTS = { jira: 'IMPORTANT: Do not post any comments - your summary will be automatically posted by system. And DO NOT update issue status if user did not request it. ALSO: do not look for other issues or any other external information unless explicitly requested by the user.', }; +/** Inline comment instructions for Junie CLI builds without `reviewTarget`: newer builds post comments themselves. */ +export const LEGACY_INLINE_COMMENT_TOOL_PROMPT = 'MANDATORY for code reviews: Use post_inline_review_comment to provide inline code review comments. IMPORTANT: If you are responding to a question in an existing review thread (user tagged you in in review thread), DO NOT use this tool - your summary will be automatically posted as a reply in that thread.'; + /** * Generates a combined prompt section describing all enabled MCP tools */ diff --git a/test/junie-tasks.test.ts b/test/junie-tasks.test.ts index 471f6ef1..12db5357 100644 --- a/test/junie-tasks.test.ts +++ b/test/junie-tasks.test.ts @@ -46,6 +46,9 @@ const REVIEW_TARGET_TYPES = ["localChanges", "remoteRequest"]; /** The keys the action is expected to send for a code review, in the order `sort()` produces. */ const EXPECTED_CODE_REVIEW_TASK_KEYS = ["description", "diffCommand", "fetchVcsInfo", "reviewTarget"]; +/** The keys CLI builds without `reviewTarget` support accept. */ +const LEGACY_CODE_REVIEW_TASK_KEYS = ["description", "diffCommand"]; + /** Asserts the payload carries nothing the CLI's strict parser would reject. */ const expectParseableByJunieCli = (input: Record) => { for (const key of Object.keys(input)) { @@ -93,7 +96,8 @@ describe("prepareJunieTask", () => { triggerPhrase: "@junie-agent", assigneeTrigger: "", labelTrigger: "", - allowedMcpServers: "" + allowedMcpServers: "", + junieVersion: "latest" }; const { inputs: _, ...restOverrides } = overrides; @@ -519,6 +523,39 @@ describe("prepareJunieTask", () => { expectParseableByJunieCli(writtenInput); }); + test("should create codeReviewTask without reviewTarget when the CLI version does not support it", async () => { + const context = createMockContext({ + eventName: "pull_request", + isPR: true, + entityNumber: 123, + inputs: { + prompt: "code-review", + // Released build without reviewTarget: sending it would abort the whole run + // with "Cannot parse input JSON" + junieVersion: "2929.5" + }, + payload: { + pull_request: { + number: 123, + title: "Test PR", + updated_at: "2024-01-01T00:00:00Z" + }, + repository: { + owner: {login: "owner"}, + name: "repo" + } + } as any + }); + const octokit = createMockOctokit(); + + const result = await prepareJunieTask(context, branchInfo, octokit, ["mcp_github_inline_comment_server"]); + + expect(Object.keys(result.codeReviewTask!).sort()).toEqual(LEGACY_CODE_REVIEW_TASK_KEYS); + // Such builds have no comment channel of their own, so the MCP tool has to be advertised + expect(result.codeReviewTask?.description).toContain("post_inline_review_comment"); + expectParseableByJunieCli(readJunieInputFile()); + }); + test("should trigger codeReviewTask from comment when inputs.prompt is empty and code-review keyword is used", async () => { const context = createMockContext({ eventName: "pull_request_review",