Skip to content

Commit 5c4d65c

Browse files
author
Marco Damonte
committed
ci failures refactor
1 parent 869dc64 commit 5c4d65c

File tree

15 files changed

+1480
-31
lines changed

15 files changed

+1480
-31
lines changed

action.yml

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -175,18 +175,21 @@ runs:
175175
EJ_AUTH_GITHUB_TOKEN: ${{ steps.prepare.outputs.EJ_AUTH_GITHUB_TOKEN }}
176176
GH_TOKEN: ${{ steps.prepare.outputs.EJ_AUTH_GITHUB_TOKEN }}
177177
MULTI_RUNS: true
178-
MATTERHORN_DEBUG_LOG: false
178+
MATTERHORN_DEBUG_LOG: true
179179
WORKING_DIR: ${{ inputs.junie_work_dir }}
180180
MODEL: ${{ inputs.model }}
181181
run: |
182182
MODEL_FLAG=""
183183
if [ -n "${MODEL}" ]; then
184184
MODEL_FLAG="--model=${MODEL}"
185185
fi
186-
OUTPUT=$(junie --cache-dir="${WORKING_DIR}" --input-format="json" --output-format="json" --auth="${CLI_TOKEN}" ${MODEL_FLAG} <<<"${JUNIE_JSON_TASK}")
187-
echo "json_output<<EOF" >> $GITHUB_OUTPUT
188-
echo "$OUTPUT" >> $GITHUB_OUTPUT
189-
echo "EOF" >> $GITHUB_OUTPUT
186+
# Ensure working directory exists
187+
mkdir -p "${WORKING_DIR}"
188+
# Set output file path FIRST (so it's available even if Junie fails)
189+
OUTPUT_FILE="${WORKING_DIR}/junie-output.json"
190+
echo "json_output_file=${OUTPUT_FILE}" >> $GITHUB_OUTPUT
191+
# Write output to file to avoid ARG_MAX limit with large outputs
192+
junie --cache-dir="${WORKING_DIR}" --input-format="json" --output-format="json" --auth="${CLI_TOKEN}" ${MODEL_FLAG} <<<"${JUNIE_JSON_TASK}" > "${OUTPUT_FILE}"
190193
191194
- name: Handle Junie results
192195
if: always() && steps.prepare.outputs.SHOULD_SKIP != 'true'
@@ -195,7 +198,7 @@ runs:
195198
run: |
196199
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/handle-results.ts
197200
env:
198-
JSON_JUNIE_OUTPUT: ${{ steps.junie-run.outputs.json_output }}
201+
JSON_JUNIE_OUTPUT_FILE: ${{ steps.junie-run.outputs.json_output_file }}
199202
PARSED_CONTEXT: ${{ steps.prepare.outputs.PARSED_CONTEXT }}
200203
INIT_COMMENT_ID: ${{ steps.prepare.outputs.INIT_COMMENT_ID }}
201204
WORKING_BRANCH: ${{ steps.prepare.outputs.WORKING_BRANCH }}
@@ -282,7 +285,7 @@ runs:
282285
COMMIT_SHA: ${{ steps.commit.outputs.commit_long_sha }}
283286
JUNIE_TITLE: ${{ steps.junie-run-results.outputs.JUNIE_TITLE }}
284287
JUNIE_SUMMARY: ${{ steps.junie-run-results.outputs.JUNIE_SUMMARY }}
285-
JSON_JUNIE_OUTPUT: ${{ steps.junie-run.outputs.json_output }}
288+
JSON_JUNIE_OUTPUT_FILE: ${{ steps.junie-run.outputs.json_output_file }}
286289
JIRA_BASE_URL: ${{ inputs.jira_base_url }}
287290
JIRA_EMAIL: ${{ inputs.jira_email }}
288291
JIRA_API_TOKEN: ${{ inputs.jira_api_token }}
@@ -306,7 +309,7 @@ runs:
306309
COMMIT_SHA: ${{ steps.commit.outputs.commit_long_sha }}
307310
JUNIE_TITLE: ${{ steps.junie-run-results.outputs.JUNIE_TITLE }}
308311
JUNIE_SUMMARY: ${{ steps.junie-run-results.outputs.JUNIE_SUMMARY }}
309-
JSON_JUNIE_OUTPUT: ${{ steps.junie-run.outputs.json_output }}
312+
JSON_JUNIE_OUTPUT_FILE: ${{ steps.junie-run.outputs.json_output_file }}
310313
JIRA_BASE_URL: ${{ inputs.jira_base_url }}
311314
JIRA_EMAIL: ${{ inputs.jira_email }}
312315
JIRA_API_TOKEN: ${{ inputs.jira_api_token }}

full_test_output.txt

Lines changed: 617 additions & 0 deletions
Large diffs are not rendered by default.

full_test_output_v2.txt

Lines changed: 526 additions & 0 deletions
Large diffs are not rendered by default.

src/constants/environment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const ENV_VARS = {
1616
OVERRIDE_GITHUB_TOKEN: "OVERRIDE_GITHUB_TOKEN",
1717
DEFAULT_WORKFLOW_TOKEN: "DEFAULT_WORKFLOW_TOKEN",
1818
JSON_JUNIE_OUTPUT: "JSON_JUNIE_OUTPUT",
19+
JSON_JUNIE_OUTPUT_FILE: "JSON_JUNIE_OUTPUT_FILE",
1920
JUNIE_WORKING_DIR: "JUNIE_WORKING_DIR",
2021
APP_TOKEN: "APP_TOKEN",
2122
TRIGGER_PHRASE: "TRIGGER_PHRASE",

src/constants/github.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ export const CODE_REVIEW_ACTION = "code-review";
2222

2323
export const CODE_REVIEW_TRIGGER_PHRASE_REGEXP = new RegExp(CODE_REVIEW_ACTION, 'i')
2424

25+
export const FIX_CI_ACTION = "fix-ci";
26+
27+
export const FIX_CI_TRIGGER_PHRASE_REGEXP = new RegExp(FIX_CI_ACTION, 'i');
28+
2529
export const JIRA_EVENT_ACTION = "jira_event";
2630

2731
export const WORKING_BRANCH_PREFIX = "junie/";
@@ -79,6 +83,72 @@ Additional instructions:
7983
`;
8084
}
8185

86+
export function createFixCIFailuresPrompt(diffPoint: string): string {
87+
const diffCommand = `gh pr diff ${diffPoint}`
88+
return `
89+
Your task is to analyze CI failures and suggest fixes WITHOUT implementing them. Follow these steps:
90+
91+
1. Gather Information
92+
- Use the 'get_pr_failed_checks_info' tool to retrieve information about failed CI/CD checks.
93+
- Read the Pull Request diff by using \`${diffCommand} | grep "^diff --git"\`. Do not write the diff to file.
94+
95+
2. If NO failed checks were found:
96+
- Submit ONLY the following message:
97+
---
98+
## ✅ CI Status
99+
100+
No failed checks found for this PR. All CI checks have passed or are still running.
101+
---
102+
103+
3. If failed checks WERE found, analyze each failure:
104+
- Open and explore relevant source files to understand the context
105+
- Identify the failing step and error message
106+
- Determine the root cause (test failure, build error, linting issue, timeout, flaky test, etc.)
107+
- Correlate the error with changes in the PR diff
108+
- Determine if the failure is related to the PR diff or a pre-existing issue
109+
110+
4. Provide Diagnosis. After exploration, submit your analysis following this template FAITHFULLY and without adding additional notes:
111+
---
112+
## 🔴 CI Failure Analysis
113+
114+
**Failed Check:** [check name]
115+
**Failed Step:** [step name if identifiable]
116+
**Error Type:** [test failure / build error / lint error / timeout / other]
117+
118+
### Error Details
119+
\`\`\`
120+
[relevant error message/stack trace - keep concise]
121+
\`\`\`
122+
123+
### Root Cause
124+
[1-3 sentences explaining why this failed]
125+
126+
### Correlation with PR Changes
127+
[Explain which files/changes in this PR likely caused the failure, or state if it appears unrelated]
128+
129+
## 🔧 Suggested Fix
130+
131+
### What needs to change
132+
[Clear description of the fix approach]
133+
134+
### Files to modify
135+
- \`path/to/file\`: [what needs to change and why]
136+
137+
### Code changes
138+
\`\`\`[language]
139+
// Suggested code snippet or pseudocode
140+
\`\`\`
141+
---
142+
143+
Additional Instructions:
144+
1. Open files or search the project as necessary to understand context before providing your diagnosis.
145+
2. Do NOT run tests, build, or make any modifications to the codebase.
146+
3. If multiple checks failed, analyze each one separately in your answer.
147+
4. If the failure appears to be a flaky test or infrastructure issue (not related to PR changes), clearly state this.
148+
5. Be specific about file paths and line numbers when suggesting fixes: \`File.ts:Line: Comment\`.
149+
6. Do not use the 'post_inline_review_comment' tool. Suggest changes only as shown in the template above`;
150+
}
151+
82152
/**
83153
* Creates a hidden marker for identifying Junie comments from a specific workflow.
84154
* This HTML comment is invisible to users but allows finding Junie comments

src/entrypoints/give-feedback.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,25 @@ import {JunieExecutionContext} from "../github/context";
44
import {ActionType} from "./handle-results";
55
import {ENV_VARS, OUTPUT_VARS} from "../constants/environment";
66
import {formatJunieSummary} from "./format-summary";
7-
import {appendFileSync} from "fs";
7+
import {appendFileSync, readFileSync, existsSync} from "fs";
88
import {buildGitHubApiClient} from "../github/api/client";
99
import {handleStepError} from "../utils/error-handler";
1010

11+
/**
12+
* Reads Junie output from file (preferred) or environment variable (fallback)
13+
* File-based approach avoids ARG_MAX limit issues with large outputs
14+
*/
15+
function readJunieOutputSafe(): string | undefined {
16+
// First try to read from file (new approach)
17+
const outputFile = process.env[ENV_VARS.JSON_JUNIE_OUTPUT_FILE];
18+
if (outputFile && existsSync(outputFile)) {
19+
return readFileSync(outputFile, 'utf-8');
20+
}
21+
22+
// Fallback to environment variable (legacy approach)
23+
return process.env[ENV_VARS.JSON_JUNIE_OUTPUT];
24+
}
25+
1126
/**
1227
* Writes feedback comment to GitHub issue/PR if initCommentId is available
1328
*/
@@ -62,7 +77,7 @@ async function generateJobSummary(isJobFailed: boolean): Promise<void> {
6277
}
6378

6479
// Try to get duration_ms from JSON_JUNIE_OUTPUT if available
65-
const jsonOutput = process.env[ENV_VARS.JSON_JUNIE_OUTPUT];
80+
const jsonOutput = readJunieOutputSafe();
6681
if (jsonOutput) {
6782
try {
6883
const parsed = JSON.parse(jsonOutput);

src/entrypoints/handle-results.ts

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {ENV_VARS, OUTPUT_VARS} from "../constants/environment";
66
import {handleStepError} from "../utils/error-handler";
77
import {isReviewOrCommentHasResolveConflictsTrigger} from "../github/validation/trigger";
88
import {sanitizeJunieOutput} from "../utils/sanitizer";
9+
import {readFileSync, existsSync} from 'fs';
910

1011
export enum ActionType {
1112
WRITE_COMMENT = 'WRITE_COMMENT',
@@ -15,18 +16,40 @@ export enum ActionType {
1516
NOTHING = 'NOTHING'
1617
}
1718

19+
/**
20+
* Reads Junie output from file (preferred) or environment variable (fallback)
21+
* File-based approach avoids ARG_MAX limit issues with large outputs
22+
*/
23+
function readJunieOutput(): string {
24+
// First try to read from file (new approach)
25+
const outputFile = process.env[ENV_VARS.JSON_JUNIE_OUTPUT_FILE];
26+
if (outputFile && existsSync(outputFile)) {
27+
console.log(`Reading Junie output from file: ${outputFile}`);
28+
const content = readFileSync(outputFile, 'utf-8');
29+
// Debug: log first 1000 chars of content to help diagnose issues
30+
console.log(`Junie output content (first 1000 chars): ${content.substring(0, 1000)}`);
31+
return content;
32+
}
33+
34+
// Fallback to environment variable (legacy approach)
35+
const envOutput = process.env[ENV_VARS.JSON_JUNIE_OUTPUT];
36+
if (envOutput) {
37+
console.log('Reading Junie output from environment variable (legacy)');
38+
return envOutput;
39+
}
40+
41+
throw new Error(
42+
`❌ Failed to retrieve Junie execution results. ` +
43+
`This could be due to:\n` +
44+
`• Junie execution did not complete successfully\n` +
45+
`• Junie output was empty or invalid\n` +
46+
`Please check the Junie execution logs for details.`
47+
);
48+
}
49+
1850
export async function handleResults() {
1951
try {
20-
const stringJunieJsonOutput = process.env[ENV_VARS.JSON_JUNIE_OUTPUT]
21-
if (!stringJunieJsonOutput) {
22-
throw new Error(
23-
`❌ Failed to retrieve Junie execution results. ` +
24-
`This could be due to:\n` +
25-
`• Junie execution did not complete successfully\n` +
26-
`• Junie output was empty or invalid\n` +
27-
`Please check the Junie execution logs for details.`
28-
);
29-
}
52+
const stringJunieJsonOutput = readJunieOutput();
3053
const junieJsonOutput = JSON.parse(stringJunieJsonOutput) as any
3154
const context = JSON.parse(process.env[OUTPUT_VARS.PARSED_CONTEXT]!) as JunieExecutionContext
3255
const isResolveConflict = context.inputs.resolveConflicts || isReviewOrCommentHasResolveConflictsTrigger(context)

src/github/context.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,16 @@ export function isIssuesAssignedEvent(
458458
return isIssuesEvent(context) && context.eventAction === "assigned";
459459
}
460460

461+
/**
462+
* Checks if the context is a workflow_run event triggered by a CI failure
463+
*/
464+
export function isWorkflowRunFailureEvent(
465+
context: JunieExecutionContext,
466+
): context is AutomationEventContext & { payload: WorkflowRunEvent } {
467+
return context.eventName === "workflow_run" &&
468+
(context.payload as WorkflowRunEvent).workflow_run?.conclusion === "failure";
469+
}
470+
461471
/**
462472
* Checks if the context is triggered by user interaction (comments, PR/issue events)
463473
*/

src/github/junie/junie-tasks.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@ import {
44
isPullRequestEvent,
55
isPullRequestReviewCommentEvent,
66
isPullRequestReviewEvent,
7+
isWorkflowRunFailureEvent,
78
JunieExecutionContext
89
} from "../context";
910
import * as core from "@actions/core";
1011
import {BranchInfo} from "../operations/branch";
1112
import {
1213
isReviewOrCommentHasCodeReviewTrigger,
14+
isReviewOrCommentHasFixCITrigger,
1315
isReviewOrCommentHasResolveConflictsTrigger
1416
} from "../validation/trigger";
1517
import {OUTPUT_VARS} from "../../constants/environment";
16-
import {CODE_REVIEW_ACTION, createCodeReviewPrompt} from "../../constants/github";
18+
import {CODE_REVIEW_ACTION, createCodeReviewPrompt, createFixCIFailuresPrompt, FIX_CI_ACTION} from "../../constants/github";
1719
import {Octokits} from "../api/client";
1820
import {NewGitHubPromptFormatter} from "./new-prompt-formatter";
1921
import {downloadAttachmentsAndRewriteText} from "./attachment-downloader";
@@ -75,12 +77,29 @@ export async function prepareJunieTask(
7577
const isCodeReviewInComment = isReviewOrCommentHasCodeReviewTrigger(context);
7678
const isCodeReview = isCodeReviewInPrompt || isCodeReviewInComment;
7779

80+
// Check if prompt contains FIX_CI_ACTION phrase, comment/review has fix-ci trigger, or workflow_run CI failure
81+
const isFixCIInPrompt = customPrompt?.includes(FIX_CI_ACTION);
82+
const isFixCIInComment = isReviewOrCommentHasFixCITrigger(context);
83+
const isFixCIFromWorkflowFailure = isWorkflowRunFailureEvent(context);
84+
const isFixCI = isFixCIInPrompt || isFixCIInComment || isFixCIFromWorkflowFailure;
85+
86+
// Debug: log fix-ci detection status
87+
console.log(`Fix-CI detection: inPrompt=${isFixCIInPrompt}, inComment=${isFixCIInComment}, fromWorkflowFailure=${isFixCIFromWorkflowFailure}, isFixCI=${isFixCI}`);
88+
7889
let promptText: string;
7990
let finalCustomPrompt = customPrompt;
8091
if (issue && isCodeReview) {
8192
const branchName = branchInfo.prBaseBranch || branchInfo.baseBranch;
8293
const diffPoint = context.isPR ? String(context.entityNumber) : branchName;
8394
finalCustomPrompt = createCodeReviewPrompt(diffPoint);
95+
console.log(`Using CODE REVIEW prompt for diffPoint: ${diffPoint}`);
96+
} else if (isFixCI) {
97+
// Fix CI can trigger with or without an associated PR/issue (e.g., workflow_run failure on a branch)
98+
const branchName = branchInfo.prBaseBranch || branchInfo.baseBranch;
99+
const diffPoint = context.isPR && context.entityNumber ? String(context.entityNumber) : branchName;
100+
finalCustomPrompt = createFixCIFailuresPrompt(diffPoint);
101+
console.log(`Using FIX-CI prompt for diffPoint: ${diffPoint}`);
102+
console.log(`Fix-CI prompt preview (first 200 chars): ${finalCustomPrompt.substring(0, 200)}`);
84103
}
85104
promptText = await formatter.generatePrompt(context, fetchedData, finalCustomPrompt, context.inputs.attachGithubContextToCustomPrompt);
86105

src/github/junie/prepare-junie.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,21 @@ import {
44
isTriggeredByUserInteraction,
55
isPushEvent,
66
isJiraWorkflowDispatchEvent,
7-
isResolveConflictsWorkflowDispatchEvent, isPullRequestEvent, isPullRequestReviewEvent, isIssueCommentEvent,
7+
isResolveConflictsWorkflowDispatchEvent, isPullRequestEvent, isPullRequestReviewEvent, isIssueCommentEvent, isWorkflowRunFailureEvent,
88
} from "../context";
99
import {checkHumanActor} from "../validation/actor";
1010
import {postJunieWorkingStatusComment} from "../operations/comments/feedback";
1111
import {initializeJunieWorkspace} from "../operations/branch";
1212
import {PrepareJunieOptions} from "./types/junie";
13-
import {detectJunieTriggerPhrase} from "../validation/trigger";
13+
import {detectJunieTriggerPhrase, isReviewOrCommentHasFixCITrigger} from "../validation/trigger";
1414
import {configureGitCredentials} from "../operations/auth";
1515
import {prepareMcpConfig} from "../../mcp/prepare-mcp-config";
1616
import {verifyRepositoryAccess} from "../validation/permissions";
1717
import {Octokits} from "../api/client";
1818
import {prepareJunieTask} from "./junie-tasks";
1919
import {prepareJunieCLIToken} from "./junie-token";
2020
import {OUTPUT_VARS} from "../../constants/environment";
21-
import {RESOLVE_CONFLICTS_ACTION} from "../../constants/github";
21+
import {RESOLVE_CONFLICTS_ACTION, FIX_CI_ACTION} from "../../constants/github";
2222
import {getJiraClient} from "../jira/client";
2323

2424
/**
@@ -62,10 +62,27 @@ export async function initializeJunieExecution({
6262

6363
// Get PR-specific info for MCP servers
6464
const prNumber = context.isPR ? context.entityNumber : undefined;
65-
const commitSha = branchInfo.headSha;
65+
66+
// For workflow_run events, use the workflow run's head_sha (the commit that was tested)
67+
// This is critical for the checks server to find the correct check runs
68+
let commitSha = branchInfo.headSha;
69+
if (isWorkflowRunFailureEvent(context)) {
70+
const workflowRunSha = (context.payload as any).workflow_run?.head_sha;
71+
if (workflowRunSha) {
72+
console.log(`Using workflow_run head_sha for checks: ${workflowRunSha}`);
73+
commitSha = workflowRunSha;
74+
}
75+
}
76+
77+
// Detect if this is a fix-ci action (needed for auto-enabling checks server)
78+
const isFixCIInPrompt = context.inputs.prompt?.includes(FIX_CI_ACTION);
79+
const isFixCIInComment = isReviewOrCommentHasFixCITrigger(context);
80+
const isFixCIFromWorkflowFailure = isWorkflowRunFailureEvent(context);
81+
const isFixCI = isFixCIInPrompt || isFixCIInComment || isFixCIFromWorkflowFailure;
6682

6783
// Prepare MCP configuration with automatic server activation
6884
// - Inline comment server: enabled for PRs (requires commitSha)
85+
// - Checks server: enabled for fix-ci action or when explicitly requested
6986
const mcpConfig = await prepareMcpConfig({
7087
junieWorkingDir: context.inputs.junieWorkingDir,
7188
allowedMcpServers: mcpServers,
@@ -74,7 +91,8 @@ export async function initializeJunieExecution({
7491
repo: context.payload.repository.name,
7592
branchInfo: branchInfo,
7693
prNumber: prNumber,
77-
commitSha: commitSha
94+
commitSha: commitSha,
95+
isFixCI: isFixCI
7896
})
7997

8098
await prepareJunieTask(context, branchInfo, octokit, mcpConfig.enabledServers)
@@ -104,6 +122,10 @@ async function shouldHandle(context: JunieExecutionContext, octokit: Octokits):
104122
return true;
105123
}
106124

125+
if (isWorkflowRunFailureEvent(context)) {
126+
return true;
127+
}
128+
107129
return isTriggeredByUserInteraction(context) && detectJunieTriggerPhrase(context) && checkHumanActor(octokit.rest, context);
108130
}
109131

0 commit comments

Comments
 (0)