@kody-ade/kody-engine-lite 0.1.66 → 0.1.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +127 -15
- package/package.json +9 -8
- package/templates/kody.yml +82 -3
package/dist/bin/cli.js
CHANGED
|
@@ -679,6 +679,45 @@ function submitPRReview(prNumber, body, event) {
|
|
|
679
679
|
logger.warn(` Failed to submit PR review: ${err}`);
|
|
680
680
|
}
|
|
681
681
|
}
|
|
682
|
+
function getCIFailureLogs(runId, maxLength = 8e3) {
|
|
683
|
+
try {
|
|
684
|
+
const logsOutput = gh([
|
|
685
|
+
"run",
|
|
686
|
+
"view",
|
|
687
|
+
String(runId),
|
|
688
|
+
"--log-failed"
|
|
689
|
+
]);
|
|
690
|
+
if (!logsOutput) return null;
|
|
691
|
+
const truncated = logsOutput.slice(-maxLength);
|
|
692
|
+
const prefix = logsOutput.length > maxLength ? "...(earlier output truncated)\n" : "";
|
|
693
|
+
return `${prefix}${truncated}`;
|
|
694
|
+
} catch (err) {
|
|
695
|
+
logger.warn(` Failed to get CI failure logs for run ${runId}: ${err}`);
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function getLatestFailedRunForBranch(branch) {
|
|
700
|
+
try {
|
|
701
|
+
const output = gh([
|
|
702
|
+
"run",
|
|
703
|
+
"list",
|
|
704
|
+
"--branch",
|
|
705
|
+
branch,
|
|
706
|
+
"--status",
|
|
707
|
+
"failure",
|
|
708
|
+
"--limit",
|
|
709
|
+
"1",
|
|
710
|
+
"--json",
|
|
711
|
+
"databaseId",
|
|
712
|
+
"--jq",
|
|
713
|
+
".[0].databaseId"
|
|
714
|
+
]);
|
|
715
|
+
return output.trim() || null;
|
|
716
|
+
} catch (err) {
|
|
717
|
+
logger.warn(` Failed to get latest failed run for branch ${branch}: ${err}`);
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
682
721
|
function getLatestKodyReviewComment(prNumber) {
|
|
683
722
|
try {
|
|
684
723
|
const output = gh([
|
|
@@ -3203,13 +3242,14 @@ function parseArgs() {
|
|
|
3203
3242
|
kody run --task-id <id> [--task "<desc>"] [--cwd <path>] [--issue-number <n>] [--complexity low|medium|high] [--feedback "<text>"] [--local] [--dry-run]
|
|
3204
3243
|
kody rerun --task-id <id> --from <stage> [--cwd <path>] [--issue-number <n>]
|
|
3205
3244
|
kody fix --task-id <id> [--cwd <path>] [--issue-number <n>] [--feedback "<text>"]
|
|
3245
|
+
kody fix-ci [--pr-number <n>] [--ci-run-id <id>] [--cwd <path>] [--issue-number <n>] [--feedback "<text>"]
|
|
3206
3246
|
kody review [--pr-number <n>] [--issue-number <n>] [--cwd <path>] [--local]
|
|
3207
3247
|
kody status --task-id <id> [--cwd <path>]
|
|
3208
3248
|
kody --help`);
|
|
3209
3249
|
process.exit(0);
|
|
3210
3250
|
}
|
|
3211
3251
|
const command2 = args2[0];
|
|
3212
|
-
if (!["run", "rerun", "fix", "status", "review"].includes(command2)) {
|
|
3252
|
+
if (!["run", "rerun", "fix", "fix-ci", "status", "review"].includes(command2)) {
|
|
3213
3253
|
console.error(`Unknown command: ${command2}`);
|
|
3214
3254
|
process.exit(1);
|
|
3215
3255
|
}
|
|
@@ -3227,7 +3267,8 @@ function parseArgs() {
|
|
|
3227
3267
|
prNumber: prStr ? parseInt(prStr, 10) : void 0,
|
|
3228
3268
|
feedback: getArg(args2, "--feedback") ?? process.env.FEEDBACK,
|
|
3229
3269
|
local: localFlag || !isCI2 && !hasFlag(args2, "--no-local"),
|
|
3230
|
-
complexity: getArg(args2, "--complexity") ?? process.env.COMPLEXITY
|
|
3270
|
+
complexity: getArg(args2, "--complexity") ?? process.env.COMPLEXITY,
|
|
3271
|
+
ciRunId: getArg(args2, "--ci-run-id") ?? process.env.CI_RUN_ID
|
|
3231
3272
|
};
|
|
3232
3273
|
}
|
|
3233
3274
|
var isCI2;
|
|
@@ -3463,7 +3504,7 @@ async function main() {
|
|
|
3463
3504
|
setGhCwd(projectDir);
|
|
3464
3505
|
logger.info(`Working directory: ${projectDir}`);
|
|
3465
3506
|
}
|
|
3466
|
-
const isPRFix = input.command === "fix" && !!input.prNumber;
|
|
3507
|
+
const isPRFix = (input.command === "fix" || input.command === "fix-ci") && !!input.prNumber;
|
|
3467
3508
|
if (input.issueNumber && input.command !== "review" && !isPRFix) {
|
|
3468
3509
|
const taskAction = resolveForIssue(input.issueNumber, projectDir);
|
|
3469
3510
|
logger.info(`Task action: ${taskAction.action}`);
|
|
@@ -3497,7 +3538,7 @@ async function main() {
|
|
|
3497
3538
|
let taskId = input.taskId;
|
|
3498
3539
|
if (!taskId) {
|
|
3499
3540
|
if (isPRFix) {
|
|
3500
|
-
taskId =
|
|
3541
|
+
taskId = `${input.command === "fix-ci" ? "fixci" : "fix"}-pr-${input.prNumber}-${generateTaskId()}`;
|
|
3501
3542
|
} else if (input.issueNumber) {
|
|
3502
3543
|
taskId = `${input.issueNumber}-${generateTaskId()}`;
|
|
3503
3544
|
} else if (input.command === "run" && input.task) {
|
|
@@ -3615,9 +3656,40 @@ ${issue.body ?? ""}`;
|
|
|
3615
3656
|
console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
|
|
3616
3657
|
process.exit(1);
|
|
3617
3658
|
}
|
|
3618
|
-
if (input.command === "fix" && !input.fromStage) {
|
|
3659
|
+
if ((input.command === "fix" || input.command === "fix-ci") && !input.fromStage) {
|
|
3619
3660
|
input.fromStage = "build";
|
|
3620
3661
|
}
|
|
3662
|
+
if (input.command === "fix-ci" && input.prNumber) {
|
|
3663
|
+
let ciRunId = input.ciRunId;
|
|
3664
|
+
if (!ciRunId && input.feedback) {
|
|
3665
|
+
const match = input.feedback.match(/Run ID:\s*(\d+)/);
|
|
3666
|
+
ciRunId = match?.[1];
|
|
3667
|
+
}
|
|
3668
|
+
if (!ciRunId) {
|
|
3669
|
+
const prDetails = getPRDetails(input.prNumber);
|
|
3670
|
+
if (prDetails) {
|
|
3671
|
+
ciRunId = getLatestFailedRunForBranch(prDetails.headBranch) ?? void 0;
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
if (ciRunId) {
|
|
3675
|
+
const ciLogs = getCIFailureLogs(ciRunId);
|
|
3676
|
+
if (ciLogs) {
|
|
3677
|
+
logger.info(` Found CI failure logs for run ${ciRunId}, injecting as feedback`);
|
|
3678
|
+
const ciContext = `## CI Failure Logs (run ${ciRunId})
|
|
3679
|
+
|
|
3680
|
+
The CI pipeline failed. Fix the code to make CI pass.
|
|
3681
|
+
|
|
3682
|
+
\`\`\`
|
|
3683
|
+
${ciLogs}
|
|
3684
|
+
\`\`\``;
|
|
3685
|
+
input.feedback = input.feedback ? `${ciContext}
|
|
3686
|
+
|
|
3687
|
+
## Additional context
|
|
3688
|
+
|
|
3689
|
+
${input.feedback}` : ciContext;
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3621
3693
|
if (input.command === "fix" && input.prNumber) {
|
|
3622
3694
|
const reviewComment = getLatestKodyReviewComment(input.prNumber);
|
|
3623
3695
|
if (reviewComment) {
|
|
@@ -3667,7 +3739,7 @@ ${input.feedback}` : reviewContext;
|
|
|
3667
3739
|
projectDir,
|
|
3668
3740
|
runners,
|
|
3669
3741
|
input: {
|
|
3670
|
-
mode: input.command === "rerun" || input.command === "fix" ? "rerun" : "full",
|
|
3742
|
+
mode: input.command === "rerun" || input.command === "fix" || input.command === "fix-ci" ? "rerun" : "full",
|
|
3671
3743
|
fromStage: input.fromStage,
|
|
3672
3744
|
dryRun: input.dryRun,
|
|
3673
3745
|
issueNumber: input.issueNumber,
|
|
@@ -4201,7 +4273,7 @@ function ghComment(issueNumber, body, cwd) {
|
|
|
4201
4273
|
} catch {
|
|
4202
4274
|
}
|
|
4203
4275
|
}
|
|
4204
|
-
function bootstrapCommand() {
|
|
4276
|
+
function bootstrapCommand(opts = { force: false }) {
|
|
4205
4277
|
const cwd = process.cwd();
|
|
4206
4278
|
const issueNumber = parseInt(process.env.ISSUE_NUMBER ?? "", 10) || 0;
|
|
4207
4279
|
console.log(`
|
|
@@ -4275,6 +4347,23 @@ ${existingFiles.join(", ")}
|
|
|
4275
4347
|
fs22.mkdirSync(memoryDir, { recursive: true });
|
|
4276
4348
|
const archPath = path21.join(memoryDir, "architecture.md");
|
|
4277
4349
|
const conventionsPath = path21.join(memoryDir, "conventions.md");
|
|
4350
|
+
const existingArch = fs22.existsSync(archPath) ? fs22.readFileSync(archPath, "utf-8") : "";
|
|
4351
|
+
const existingConv = fs22.existsSync(conventionsPath) ? fs22.readFileSync(conventionsPath, "utf-8") : "";
|
|
4352
|
+
const hasExisting = !!(existingArch || existingConv);
|
|
4353
|
+
const extendInstruction = hasExisting && !opts.force ? `
|
|
4354
|
+
## Existing Documentation (EXTEND, do not replace)
|
|
4355
|
+
You are UPDATING existing documentation. Follow these rules strictly:
|
|
4356
|
+
- PRESERVE all existing sections and content that are still accurate
|
|
4357
|
+
- REMOVE only lines that reference files, patterns, or dependencies that no longer exist in the project
|
|
4358
|
+
- APPEND new sections or lines for newly discovered patterns, files, or conventions
|
|
4359
|
+
- Do NOT rewrite sections that are still correct \u2014 keep them verbatim
|
|
4360
|
+
|
|
4361
|
+
### Existing architecture.md:
|
|
4362
|
+
${existingArch}
|
|
4363
|
+
|
|
4364
|
+
### Existing conventions.md:
|
|
4365
|
+
${existingConv}
|
|
4366
|
+
` : "";
|
|
4278
4367
|
const memoryPrompt = `You are analyzing a project to generate documentation for an autonomous SDLC pipeline.
|
|
4279
4368
|
|
|
4280
4369
|
Given this project context, output ONLY a JSON object with EXACTLY this structure:
|
|
@@ -4294,7 +4383,7 @@ Rules for conventions (markdown string):
|
|
|
4294
4383
|
- Extract actual patterns from the project
|
|
4295
4384
|
- If CLAUDE.md exists, reference it
|
|
4296
4385
|
- Keep under 30 lines
|
|
4297
|
-
|
|
4386
|
+
${extendInstruction}
|
|
4298
4387
|
Output ONLY valid JSON. No markdown fences. No explanation.
|
|
4299
4388
|
|
|
4300
4389
|
${repoContext}`;
|
|
@@ -4352,11 +4441,16 @@ ${detected.join("\n")}
|
|
|
4352
4441
|
console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
|
|
4353
4442
|
continue;
|
|
4354
4443
|
}
|
|
4444
|
+
const stepOutputPath = path21.join(stepsDir, `${stage}.md`);
|
|
4445
|
+
if (fs22.existsSync(stepOutputPath) && !opts.force) {
|
|
4446
|
+
console.log(` \u25CB ${stage}.md \u2014 already exists (use --force to regenerate)`);
|
|
4447
|
+
continue;
|
|
4448
|
+
}
|
|
4355
4449
|
const defaultPrompt = fs22.readFileSync(templatePath, "utf-8");
|
|
4356
4450
|
const contextPlaceholder = "{{TASK_CONTEXT}}";
|
|
4357
4451
|
const placeholderIdx = defaultPrompt.indexOf(contextPlaceholder);
|
|
4358
4452
|
if (placeholderIdx === -1) {
|
|
4359
|
-
fs22.copyFileSync(templatePath,
|
|
4453
|
+
fs22.copyFileSync(templatePath, stepOutputPath);
|
|
4360
4454
|
stepCount++;
|
|
4361
4455
|
console.log(` \u2713 ${stage}.md`);
|
|
4362
4456
|
continue;
|
|
@@ -4413,12 +4507,12 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
4413
4507
|
let cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
|
|
4414
4508
|
cleaned = cleaned.replace(/\n*\{\{TASK_CONTEXT\}\}\s*$/, "").trimEnd();
|
|
4415
4509
|
const finalPrompt = cleaned + "\n\n" + afterPlaceholder;
|
|
4416
|
-
fs22.writeFileSync(
|
|
4510
|
+
fs22.writeFileSync(stepOutputPath, finalPrompt);
|
|
4417
4511
|
stepCount++;
|
|
4418
4512
|
console.log(` \u2713 ${stage}.md`);
|
|
4419
4513
|
} catch {
|
|
4420
4514
|
console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
|
|
4421
|
-
fs22.copyFileSync(templatePath,
|
|
4515
|
+
fs22.copyFileSync(templatePath, stepOutputPath);
|
|
4422
4516
|
stepCount++;
|
|
4423
4517
|
}
|
|
4424
4518
|
}
|
|
@@ -4685,8 +4779,26 @@ function installSkillsForProject(cwd) {
|
|
|
4685
4779
|
console.log(" \u25CB No skills to install (no frontend framework detected)");
|
|
4686
4780
|
return [];
|
|
4687
4781
|
}
|
|
4782
|
+
let installedSkills = {};
|
|
4783
|
+
const lockPath = path21.join(cwd, "skills-lock.json");
|
|
4784
|
+
if (fs22.existsSync(lockPath)) {
|
|
4785
|
+
try {
|
|
4786
|
+
const lock = JSON.parse(fs22.readFileSync(lockPath, "utf-8"));
|
|
4787
|
+
installedSkills = lock.skills ?? {};
|
|
4788
|
+
} catch {
|
|
4789
|
+
}
|
|
4790
|
+
}
|
|
4688
4791
|
const installedPaths = [];
|
|
4689
4792
|
for (const skill of skills) {
|
|
4793
|
+
const skillName = skill.package.split("@").pop() ?? "";
|
|
4794
|
+
if (skillName in installedSkills) {
|
|
4795
|
+
console.log(` \u25CB ${skill.label} \u2014 already installed`);
|
|
4796
|
+
const agentPath = `.agents/skills/${skillName}`;
|
|
4797
|
+
const claudePath = `.claude/skills/${skillName}`;
|
|
4798
|
+
if (fs22.existsSync(path21.join(cwd, agentPath))) installedPaths.push(agentPath);
|
|
4799
|
+
if (fs22.existsSync(path21.join(cwd, claudePath))) installedPaths.push(claudePath);
|
|
4800
|
+
continue;
|
|
4801
|
+
}
|
|
4690
4802
|
try {
|
|
4691
4803
|
console.log(` Installing: ${skill.label} (${skill.package})`);
|
|
4692
4804
|
execFileSync11("npx", ["skills", "add", skill.package, "--yes"], {
|
|
@@ -4695,9 +4807,9 @@ function installSkillsForProject(cwd) {
|
|
|
4695
4807
|
timeout: 6e4,
|
|
4696
4808
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4697
4809
|
});
|
|
4698
|
-
const
|
|
4699
|
-
const agentPath = `.agents/skills/${
|
|
4700
|
-
const claudePath = `.claude/skills/${
|
|
4810
|
+
const skillName2 = skill.package.split("@").pop() ?? "";
|
|
4811
|
+
const agentPath = `.agents/skills/${skillName2}`;
|
|
4812
|
+
const claudePath = `.claude/skills/${skillName2}`;
|
|
4701
4813
|
if (fs22.existsSync(path21.join(cwd, agentPath))) installedPaths.push(agentPath);
|
|
4702
4814
|
if (fs22.existsSync(path21.join(cwd, claudePath))) installedPaths.push(claudePath);
|
|
4703
4815
|
console.log(` \u2713 ${skill.label}`);
|
|
@@ -4712,7 +4824,7 @@ var command = args[0];
|
|
|
4712
4824
|
if (command === "init") {
|
|
4713
4825
|
initCommand({ force: args.includes("--force") });
|
|
4714
4826
|
} else if (command === "bootstrap") {
|
|
4715
|
-
bootstrapCommand();
|
|
4827
|
+
bootstrapCommand({ force: args.includes("--force") });
|
|
4716
4828
|
} else if (command === "version" || command === "--version" || command === "-v") {
|
|
4717
4829
|
console.log(getVersion());
|
|
4718
4830
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kody-ade/kody-engine-lite",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.68",
|
|
4
4
|
"description": "Autonomous SDLC pipeline: Kody orchestration + Claude Code + LiteLLM",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
"templates",
|
|
14
14
|
"kody.config.schema.json"
|
|
15
15
|
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"kody": "tsx src/entry.ts",
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"prepublishOnly": "pnpm build"
|
|
22
|
+
},
|
|
16
23
|
"dependencies": {
|
|
17
24
|
"dotenv": "^16.4.7"
|
|
18
25
|
},
|
|
@@ -25,11 +32,5 @@
|
|
|
25
32
|
},
|
|
26
33
|
"engines": {
|
|
27
34
|
"node": ">=22"
|
|
28
|
-
},
|
|
29
|
-
"scripts": {
|
|
30
|
-
"kody": "tsx src/entry.ts",
|
|
31
|
-
"build": "tsup",
|
|
32
|
-
"test": "vitest run",
|
|
33
|
-
"typecheck": "tsc --noEmit"
|
|
34
35
|
}
|
|
35
|
-
}
|
|
36
|
+
}
|
package/templates/kody.yml
CHANGED
|
@@ -28,12 +28,16 @@ on:
|
|
|
28
28
|
pull_request_review:
|
|
29
29
|
types: [submitted]
|
|
30
30
|
|
|
31
|
+
workflow_run:
|
|
32
|
+
workflows: ["CI"]
|
|
33
|
+
types: [completed]
|
|
34
|
+
|
|
31
35
|
push:
|
|
32
36
|
branches: [main, dev]
|
|
33
37
|
paths: ["src/**", "kody.config.json", "package.json"]
|
|
34
38
|
|
|
35
39
|
concurrency:
|
|
36
|
-
group: kody-${{ github.event.inputs.task_id || github.event.issue.number || github.event.pull_request.number || github.sha }}
|
|
40
|
+
group: kody-${{ github.event.inputs.task_id || github.event.issue.number || github.event.pull_request.number || github.event.workflow_run.id || github.sha }}
|
|
37
41
|
cancel-in-progress: false
|
|
38
42
|
|
|
39
43
|
permissions:
|
|
@@ -56,6 +60,7 @@ jobs:
|
|
|
56
60
|
issue_number: ${{ steps.parse.outputs.issue_number }}
|
|
57
61
|
pr_number: ${{ steps.parse.outputs.pr_number }}
|
|
58
62
|
feedback: ${{ steps.parse.outputs.feedback }}
|
|
63
|
+
ci_run_id: ${{ steps.parse.outputs.ci_run_id }}
|
|
59
64
|
valid: ${{ steps.parse.outputs.valid }}
|
|
60
65
|
steps:
|
|
61
66
|
- uses: actions/setup-node@v4
|
|
@@ -109,7 +114,7 @@ jobs:
|
|
|
109
114
|
|
|
110
115
|
# Validate mode
|
|
111
116
|
case "$MODE" in
|
|
112
|
-
full|rerun|fix|status|approve|review|bootstrap) ;;
|
|
117
|
+
full|rerun|fix|fix-ci|status|approve|review|bootstrap) ;;
|
|
113
118
|
*)
|
|
114
119
|
# If first arg isn't a mode, it might be a task-id or nothing
|
|
115
120
|
if [ -n "$MODE" ] && [ "$MODE" != "" ]; then
|
|
@@ -139,6 +144,15 @@ jobs:
|
|
|
139
144
|
# Leave TASK_ID empty — entry.ts finds latest task for issue
|
|
140
145
|
fi
|
|
141
146
|
|
|
147
|
+
# fix-ci: extract body as feedback + CI run ID
|
|
148
|
+
if [ "$MODE" = "fix-ci" ]; then
|
|
149
|
+
FIX_CI_BODY=$(echo "$BODY" | sed -n '/\(@kody\|\/kody\)\s*fix-ci/,$p' | tail -n +2)
|
|
150
|
+
if [ -n "$FIX_CI_BODY" ]; then
|
|
151
|
+
FEEDBACK="$FIX_CI_BODY"
|
|
152
|
+
fi
|
|
153
|
+
CI_RUN_ID=$(echo "$FIX_CI_BODY" | grep -oP 'Run ID:\s*\K\d+' || echo "")
|
|
154
|
+
fi
|
|
155
|
+
|
|
142
156
|
# Bootstrap mode: set task-id and skip normal pipeline
|
|
143
157
|
if [ "$MODE" = "bootstrap" ]; then
|
|
144
158
|
TASK_ID="bootstrap-$(date +%y%m%d-%H%M%S)"
|
|
@@ -170,6 +184,7 @@ jobs:
|
|
|
170
184
|
echo "$FEEDBACK"
|
|
171
185
|
echo "KODY_EOF"
|
|
172
186
|
} >> $GITHUB_OUTPUT
|
|
187
|
+
echo "ci_run_id=${CI_RUN_ID:-}" >> $GITHUB_OUTPUT
|
|
173
188
|
echo "valid=true" >> $GITHUB_OUTPUT
|
|
174
189
|
|
|
175
190
|
# ─── Orchestrate ─────────────────────────────────────────────────────────────
|
|
@@ -198,7 +213,7 @@ jobs:
|
|
|
198
213
|
token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
|
|
199
214
|
|
|
200
215
|
- name: Checkout PR branch (for fix/rerun/review on PRs)
|
|
201
|
-
if: github.event.issue.pull_request && (needs.parse.outputs.mode == 'fix' || needs.parse.outputs.mode == 'rerun' || needs.parse.outputs.mode == 'review')
|
|
216
|
+
if: github.event.issue.pull_request && (needs.parse.outputs.mode == 'fix' || needs.parse.outputs.mode == 'fix-ci' || needs.parse.outputs.mode == 'rerun' || needs.parse.outputs.mode == 'review')
|
|
202
217
|
env:
|
|
203
218
|
GH_TOKEN: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
|
|
204
219
|
run: |
|
|
@@ -243,6 +258,7 @@ jobs:
|
|
|
243
258
|
ISSUE_NUMBER: ${{ github.event.inputs.issue_number || needs.parse.outputs.issue_number }}
|
|
244
259
|
PR_NUMBER: ${{ needs.parse.outputs.pr_number }}
|
|
245
260
|
FEEDBACK: ${{ github.event.inputs.feedback || needs.parse.outputs.feedback }}
|
|
261
|
+
CI_RUN_ID: ${{ needs.parse.outputs.ci_run_id }}
|
|
246
262
|
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
|
|
247
263
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
248
264
|
run: |
|
|
@@ -253,6 +269,7 @@ jobs:
|
|
|
253
269
|
CMD="run"
|
|
254
270
|
[ "$MODE" = "rerun" ] && CMD="rerun"
|
|
255
271
|
[ "$MODE" = "fix" ] && CMD="fix"
|
|
272
|
+
[ "$MODE" = "fix-ci" ] && CMD="fix-ci"
|
|
256
273
|
[ "$MODE" = "review" ] && CMD="review"
|
|
257
274
|
ARGS="--issue-number $ISSUE_NUMBER"
|
|
258
275
|
[ -n "$TASK_ID" ] && ARGS="$ARGS --task-id $TASK_ID"
|
|
@@ -338,6 +355,68 @@ jobs:
|
|
|
338
355
|
});
|
|
339
356
|
}
|
|
340
357
|
|
|
358
|
+
# ─── Fix-CI Auto-trigger (workflow_run trigger) ─────────────────────────────
|
|
359
|
+
fix-ci-trigger:
|
|
360
|
+
if: >-
|
|
361
|
+
github.event_name == 'workflow_run' &&
|
|
362
|
+
github.event.workflow_run.conclusion == 'failure' &&
|
|
363
|
+
github.event.workflow_run.event == 'pull_request' &&
|
|
364
|
+
github.event.workflow_run.pull_requests[0]
|
|
365
|
+
runs-on: ubuntu-latest
|
|
366
|
+
timeout-minutes: 5
|
|
367
|
+
permissions:
|
|
368
|
+
issues: write
|
|
369
|
+
pull-requests: write
|
|
370
|
+
steps:
|
|
371
|
+
- name: Check loop guard and post fix-ci comment
|
|
372
|
+
uses: actions/github-script@v7
|
|
373
|
+
with:
|
|
374
|
+
script: |
|
|
375
|
+
const pr = context.payload.workflow_run.pull_requests[0];
|
|
376
|
+
const prNumber = pr.number;
|
|
377
|
+
|
|
378
|
+
// Check recent comments for existing fix-ci attempt (last 24h)
|
|
379
|
+
const comments = await github.rest.issues.listComments({
|
|
380
|
+
owner: context.repo.owner,
|
|
381
|
+
repo: context.repo.repo,
|
|
382
|
+
issue_number: prNumber,
|
|
383
|
+
per_page: 30,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
387
|
+
const recentFixCi = comments.data.filter(
|
|
388
|
+
(c) => c.body.includes('@kody fix-ci') && new Date(c.created_at) > oneDayAgo
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
if (recentFixCi.length >= 1) {
|
|
392
|
+
core.info('Loop guard: @kody fix-ci already commented in last 24h, skipping');
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Check if last commit was from a bot (kody's previous fix attempt)
|
|
397
|
+
const commits = await github.rest.pulls.listCommits({
|
|
398
|
+
owner: context.repo.owner,
|
|
399
|
+
repo: context.repo.repo,
|
|
400
|
+
pull_number: prNumber,
|
|
401
|
+
per_page: 1,
|
|
402
|
+
});
|
|
403
|
+
const lastAuthor = commits.data[commits.data.length - 1]?.commit?.author?.name;
|
|
404
|
+
if (lastAuthor === 'github-actions[bot]' || lastAuthor === 'kody[bot]') {
|
|
405
|
+
core.info('Loop guard: last commit from bot, skipping');
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Post fix-ci comment
|
|
410
|
+
const runId = context.payload.workflow_run.id;
|
|
411
|
+
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
|
|
412
|
+
await github.rest.issues.createComment({
|
|
413
|
+
owner: context.repo.owner,
|
|
414
|
+
repo: context.repo.repo,
|
|
415
|
+
issue_number: prNumber,
|
|
416
|
+
body: `@kody fix-ci\nCI failed: [View logs](${runUrl})\nRun ID: ${runId}`,
|
|
417
|
+
});
|
|
418
|
+
core.info(`Posted @kody fix-ci on PR #${prNumber} for run ${runId}`);
|
|
419
|
+
|
|
341
420
|
# ─── Smoke Test (push trigger) ──────────────────────────────────────────────
|
|
342
421
|
smoke-test:
|
|
343
422
|
if: github.event_name == 'push'
|