ira-review 3.1.0 → 3.1.2
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/README.github.md +15 -0
- package/README.md +2 -0
- package/README.npm.md +2 -0
- package/dist/cli.js +61 -6
- package/dist/index.cjs +55 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +55 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.github.md
CHANGED
|
@@ -135,6 +135,12 @@ IRA fetches the linked JIRA ticket, extracts the acceptance criteria, and uses t
|
|
|
135
135
|
|
|
136
136
|
This is the feature that catches "does this actually match the ticket?" before a human has to ask.
|
|
137
137
|
|
|
138
|
+
When a JIRA ticket has **no acceptance criteria at all**, IRA generates suggested ACs from the diff and (by default) posts them as a comment on the JIRA ticket for the Product Owner to review and refine. If your CI environment cannot or should not write back to JIRA — for example, the JIRA service account is read-only, or org policy forbids automated JIRA writes — pass `--no-post-acs-to-jira` (or set `IRA_POST_ACS_TO_JIRA=false`). The suggestions still render in the PR summary under **📝 Suggested Acceptance Criteria**, so reviewers see them either way; only the JIRA write is suppressed.
|
|
139
|
+
|
|
140
|
+
### "All Clear" PR Summary
|
|
141
|
+
|
|
142
|
+
When IRA finishes a review with **zero issues** found and **JIRA acceptance criteria are 100% covered** (or the ticket needs no ACs), the PR summary leads with a celebratory ✅ banner: a confident, specific signal that automated review passed, alongside an explicit reminder that **human reviewer approval is still required before merge**. The banner is suppressed when an AC gap exists so the summary never reads "safe to approve" while a requirements gap is still visible — IRA augments your code review process, it doesn't replace it.
|
|
143
|
+
|
|
138
144
|
### Inline AI Comments
|
|
139
145
|
|
|
140
146
|
Each issue is posted as an inline comment on the exact line in the PR, containing:
|
|
@@ -436,6 +442,15 @@ Tips for Jenkins / corporate networks:
|
|
|
436
442
|
- **Comment style** — use `--comment-style compact` (default) for terse,
|
|
437
443
|
severity-first inline comments. `--comment-style detailed` keeps the legacy
|
|
438
444
|
Explanation / Impact / Suggested Fix block.
|
|
445
|
+
- **Don't write to JIRA** — pass `--no-post-acs-to-jira` (or set
|
|
446
|
+
`IRA_POST_ACS_TO_JIRA=false`) when the JIRA token is read-only or org policy
|
|
447
|
+
forbids automated comments on tickets. AI-generated AC suggestions still
|
|
448
|
+
appear in the PR summary; only the JIRA write is suppressed.
|
|
449
|
+
- **PowerShell stability on Windows agents** — wrap the `ira-review` invocation
|
|
450
|
+
with `2>&1 | ForEach-Object { Write-Host $_ }` and check `$LASTEXITCODE`
|
|
451
|
+
explicitly. PowerShell's default `$ErrorActionPreference = 'Stop'` treats any
|
|
452
|
+
native-command stderr as a terminating `NativeCommandError`; merging stderr
|
|
453
|
+
into stdout prevents harmless deprecation notices from aborting the pipeline.
|
|
439
454
|
|
|
440
455
|
---
|
|
441
456
|
|
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ Each issue is posted as an inline comment on the exact PR line with explanation,
|
|
|
42
42
|
- Two-pass critical review (`--ai-model-critical`) — bulk pass uses your everyday model; only `CRITICAL`/`BLOCKER` findings are re-run against a stronger model, keeping premium-request cost low while preserving deep analysis on what matters
|
|
43
43
|
- JIRA acceptance criteria validation with per-criterion pass/fail and edge case detection
|
|
44
44
|
- JIRA AC auto-detection — finds AC from custom field or description automatically
|
|
45
|
+
- "All Clear" PR summary block — celebratory ✅ banner when zero issues are found and JIRA AC coverage is 100%, with a clear "human reviewer approval is still required before merge" reminder. Suppressed automatically if any AC gap exists, so the summary never claims "safe to approve" while requirements are unmet.
|
|
45
46
|
- Custom team review rules via `.ira-rules.json` (see below)
|
|
46
47
|
- Test case generation from JIRA tickets (Jest, Vitest, Playwright, etc.)
|
|
47
48
|
- Comment deduplication across re-runs
|
|
@@ -151,6 +152,7 @@ All optional. IRA works with just an SCM token and an AI key.
|
|
|
151
152
|
| OpenAI-compatible gateway | `--ai-base-url https://your-llm-proxy/v1` (GitHub Models, LiteLLM, internal proxy…) |
|
|
152
153
|
| Rules from URL (no checkout) | `--rules-url https://bitbucket.example.com/.../.ira-rules.json` |
|
|
153
154
|
| Compact / detailed comments | `--comment-style compact` (default) or `--comment-style detailed` |
|
|
155
|
+
| Don't post AI-generated ACs to JIRA | `--no-post-acs-to-jira` (env: `IRA_POST_ACS_TO_JIRA=false`) — suggestions still render in the PR summary; only the JIRA write is skipped |
|
|
154
156
|
|
|
155
157
|
---
|
|
156
158
|
|
package/README.npm.md
CHANGED
|
@@ -42,6 +42,7 @@ Each issue is posted as an inline comment on the exact PR line with explanation,
|
|
|
42
42
|
- Two-pass critical review (`--ai-model-critical`) — bulk pass uses your everyday model; only `CRITICAL`/`BLOCKER` findings are re-run against a stronger model, keeping premium-request cost low while preserving deep analysis on what matters
|
|
43
43
|
- JIRA acceptance criteria validation with per-criterion pass/fail and edge case detection
|
|
44
44
|
- JIRA AC auto-detection — finds AC from custom field or description automatically
|
|
45
|
+
- "All Clear" PR summary block — celebratory ✅ banner when zero issues are found and JIRA AC coverage is 100%, with a clear "human reviewer approval is still required before merge" reminder. Suppressed automatically if any AC gap exists, so the summary never claims "safe to approve" while requirements are unmet.
|
|
45
46
|
- Custom team review rules via `.ira-rules.json` (see below)
|
|
46
47
|
- Test case generation from JIRA tickets (Jest, Vitest, Playwright, etc.)
|
|
47
48
|
- Comment deduplication across re-runs
|
|
@@ -151,6 +152,7 @@ All optional. IRA works with just an SCM token and an AI key.
|
|
|
151
152
|
| OpenAI-compatible gateway | `--ai-base-url https://your-llm-proxy/v1` (GitHub Models, LiteLLM, internal proxy…) |
|
|
152
153
|
| Rules from URL (no checkout) | `--rules-url https://bitbucket.example.com/.../.ira-rules.json` |
|
|
153
154
|
| Compact / detailed comments | `--comment-style compact` (default) or `--comment-style detailed` |
|
|
155
|
+
| Don't post AI-generated ACs to JIRA | `--no-post-acs-to-jira` (env: `IRA_POST_ACS_TO_JIRA=false`) — suggestions still render in the PR summary; only the JIRA write is skipped |
|
|
154
156
|
|
|
155
157
|
---
|
|
156
158
|
|
package/dist/cli.js
CHANGED
|
@@ -1295,10 +1295,17 @@ var BitbucketServerClient = class {
|
|
|
1295
1295
|
});
|
|
1296
1296
|
}
|
|
1297
1297
|
async getIssueComments(pullRequestId) {
|
|
1298
|
+
const collect = (node, sink) => {
|
|
1299
|
+
if (!node) return;
|
|
1300
|
+
if (typeof node.text === "string" && node.text.length > 0) sink.push(node.text);
|
|
1301
|
+
if (Array.isArray(node.comments)) {
|
|
1302
|
+
for (const child of node.comments) collect(child, sink);
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1298
1305
|
const bodies = [];
|
|
1299
1306
|
let start = 0;
|
|
1300
1307
|
while (true) {
|
|
1301
|
-
const url = this.prUrl(pullRequestId, `/
|
|
1308
|
+
const url = this.prUrl(pullRequestId, `/activities?start=${start}&limit=100`);
|
|
1302
1309
|
const data = await withRetry(async () => {
|
|
1303
1310
|
const response = await fetchWithTimeout(url, { headers: this.headers });
|
|
1304
1311
|
if (!response.ok) {
|
|
@@ -1310,7 +1317,9 @@ var BitbucketServerClient = class {
|
|
|
1310
1317
|
}
|
|
1311
1318
|
return await response.json();
|
|
1312
1319
|
});
|
|
1313
|
-
for (const
|
|
1320
|
+
for (const activity of data.values) {
|
|
1321
|
+
if (activity.action === "COMMENTED") collect(activity.comment, bodies);
|
|
1322
|
+
}
|
|
1314
1323
|
if (data.isLastPage) break;
|
|
1315
1324
|
start = data.nextPageStart ?? start + 100;
|
|
1316
1325
|
}
|
|
@@ -2615,6 +2624,29 @@ function buildSummary2(result) {
|
|
|
2615
2624
|
lines.push("");
|
|
2616
2625
|
}
|
|
2617
2626
|
}
|
|
2627
|
+
const acGapExists = result.requirementCompletion && result.requirementCompletion.completionPercentage < 100 || result.acceptanceValidation && !result.acceptanceValidation.overallPass;
|
|
2628
|
+
if (result.comments.length === 0 && !acGapExists) {
|
|
2629
|
+
const fw = result.framework ?? "your stack";
|
|
2630
|
+
const acLine = result.requirementCompletion ? `Acceptance criteria for **${result.requirementCompletion.jiraKey}**: **${result.requirementCompletion.completionPercentage}% covered** (${result.requirementCompletion.metCriteria}/${result.requirementCompletion.totalCriteria}).` : result.acceptanceValidation ? `Acceptance criteria for **${result.acceptanceValidation.jiraKey}**: ${result.acceptanceValidation.overallPass ? "**all met** \u2705" : "**partially met** \u2014 see the JIRA section above"}.` : result.acGeneration && result.acGeneration.criteria.length > 0 ? result.acGeneration.postedToJira ? `\u{1F4DD} No acceptance criteria found on **${result.acGeneration.jiraKey}** \u2014 IRA generated **${result.acGeneration.totalCriteria} suggested AC${result.acGeneration.totalCriteria === 1 ? "" : "s"}** and posted them as a comment on the JIRA ticket for the Product Owner / requirement author to review and refine.` : `\u{1F4DD} No acceptance criteria found on **${result.acGeneration.jiraKey}** \u2014 IRA generated **${result.acGeneration.totalCriteria} suggested AC${result.acGeneration.totalCriteria === 1 ? "" : "s"}** (see the Suggested Acceptance Criteria section below).` : null;
|
|
2631
|
+
lines.push("## \u2705 All Clear \u2014 No Issues Found");
|
|
2632
|
+
lines.push("");
|
|
2633
|
+
lines.push(`> \u{1F389} **Nice work on PR #${result.pullRequestId}!**`);
|
|
2634
|
+
lines.push(`>`);
|
|
2635
|
+
lines.push(`> IRA scanned every changed file across **${fw}** and didn't surface a single concern.`);
|
|
2636
|
+
if (acLine) {
|
|
2637
|
+
lines.push(`>`);
|
|
2638
|
+
lines.push(`> ${acLine}`);
|
|
2639
|
+
}
|
|
2640
|
+
if (result.risk) {
|
|
2641
|
+
lines.push(`>`);
|
|
2642
|
+
lines.push(`> Risk score: **${result.risk.score}/${result.risk.maxScore}** (${result.risk.level}).`);
|
|
2643
|
+
}
|
|
2644
|
+
lines.push(`>`);
|
|
2645
|
+
lines.push(`> \u2705 **Safe to approve from an automated-review standpoint.**`);
|
|
2646
|
+
lines.push(`>`);
|
|
2647
|
+
lines.push(`> \u{1F465} **Human reviewer approval is still required before merge.** IRA augments your code review process \u2014 it doesn't replace it. Please ensure your team's review and approval requirements have been met before merging.`);
|
|
2648
|
+
lines.push("");
|
|
2649
|
+
}
|
|
2618
2650
|
lines.push("## Overview");
|
|
2619
2651
|
lines.push("");
|
|
2620
2652
|
lines.push(`| Metric | Value |`);
|
|
@@ -2862,7 +2894,10 @@ var Notifier = class {
|
|
|
2862
2894
|
import { execSync as execSync2 } from "child_process";
|
|
2863
2895
|
function resolveGitRoot() {
|
|
2864
2896
|
try {
|
|
2865
|
-
return execSync2("git rev-parse --show-toplevel", {
|
|
2897
|
+
return execSync2("git rev-parse --show-toplevel", {
|
|
2898
|
+
encoding: "utf-8",
|
|
2899
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2900
|
+
}).trim();
|
|
2866
2901
|
} catch {
|
|
2867
2902
|
return process.cwd();
|
|
2868
2903
|
}
|
|
@@ -3403,7 +3438,11 @@ Cause: ${warnings[warnings.length - 1]}` : "";
|
|
|
3403
3438
|
let commitMessages = [];
|
|
3404
3439
|
try {
|
|
3405
3440
|
const { execSync: execSync6 } = await import("child_process");
|
|
3406
|
-
const gitLog = execSync6("git log --oneline -20 --no-decorate", {
|
|
3441
|
+
const gitLog = execSync6("git log --oneline -20 --no-decorate", {
|
|
3442
|
+
cwd: repoPath,
|
|
3443
|
+
encoding: "utf-8",
|
|
3444
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3445
|
+
});
|
|
3407
3446
|
commitMessages = gitLog.trim().split("\n").filter(Boolean);
|
|
3408
3447
|
} catch {
|
|
3409
3448
|
}
|
|
@@ -3422,15 +3461,23 @@ Cause: ${warnings[warnings.length - 1]}` : "";
|
|
|
3422
3461
|
if (acGeneration.parseWarning) {
|
|
3423
3462
|
warnings.push(acGeneration.parseWarning);
|
|
3424
3463
|
}
|
|
3425
|
-
|
|
3464
|
+
const postingDisabled = this.config.postAcsToJira === false;
|
|
3465
|
+
if (acGeneration.criteria.length > 0 && !this.config.dryRun && !postingDisabled) {
|
|
3426
3466
|
try {
|
|
3427
3467
|
const commentBody = formatACsForJiraComment(acGeneration, pullRequestId);
|
|
3428
3468
|
await jiraClient.addComment(this.config.jiraTicket, commentBody);
|
|
3429
3469
|
console.log(` Posted ${acGeneration.totalCriteria} suggested ACs to ${this.config.jiraTicket}`);
|
|
3470
|
+
acGeneration.postedToJira = true;
|
|
3430
3471
|
} catch (error) {
|
|
3431
3472
|
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
3432
3473
|
warnings.push(`Failed to post AC suggestions to JIRA: ${msg}`);
|
|
3474
|
+
acGeneration.postedToJira = false;
|
|
3433
3475
|
}
|
|
3476
|
+
} else if (postingDisabled && acGeneration.criteria.length > 0) {
|
|
3477
|
+
console.log(` Skipped posting ${acGeneration.totalCriteria} suggested ACs to ${this.config.jiraTicket} (--no-post-acs-to-jira). See PR summary.`);
|
|
3478
|
+
acGeneration.postedToJira = false;
|
|
3479
|
+
} else {
|
|
3480
|
+
acGeneration.postedToJira = false;
|
|
3434
3481
|
}
|
|
3435
3482
|
}
|
|
3436
3483
|
}
|
|
@@ -3644,6 +3691,8 @@ function resolveConfigFromEnv(overrides = {}) {
|
|
|
3644
3691
|
}
|
|
3645
3692
|
const jiraAcSource = overrides.jiraAcSource ?? optionalEnv("IRA_JIRA_AC_SOURCE");
|
|
3646
3693
|
const jiraTicket = overrides.jiraTicket ?? optionalEnv("IRA_JIRA_TICKET");
|
|
3694
|
+
const postAcsToJiraEnv = optionalEnv("IRA_POST_ACS_TO_JIRA");
|
|
3695
|
+
const postAcsToJira = overrides.postAcsToJira === false ? false : postAcsToJiraEnv && /^(false|0|no|off)$/i.test(postAcsToJiraEnv) ? false : void 0;
|
|
3647
3696
|
const commentStyleRaw = overrides.commentStyle ?? optionalEnv("IRA_COMMENT_STYLE");
|
|
3648
3697
|
if (commentStyleRaw && commentStyleRaw !== "compact" && commentStyleRaw !== "detailed") {
|
|
3649
3698
|
throw new Error(`Invalid comment-style: "${commentStyleRaw}". Must be "compact" or "detailed".`);
|
|
@@ -3677,6 +3726,7 @@ function resolveConfigFromEnv(overrides = {}) {
|
|
|
3677
3726
|
...overrides.generateTests && { generateTests: overrides.generateTests },
|
|
3678
3727
|
...overrides.testFramework && { testFramework: overrides.testFramework },
|
|
3679
3728
|
...jiraAcSource && { jiraAcSource },
|
|
3729
|
+
...postAcsToJira === false && { postAcsToJira: false },
|
|
3680
3730
|
...commentStyle && { commentStyle },
|
|
3681
3731
|
...rulesUrl && { rulesUrl }
|
|
3682
3732
|
};
|
|
@@ -4030,7 +4080,7 @@ function logEnvironmentInfo(config) {
|
|
|
4030
4080
|
}
|
|
4031
4081
|
var program = new Command();
|
|
4032
4082
|
program.name("ira-review").description("AI-powered PR review tool with SonarQube + GitHub/Bitbucket integration").version("3.1.0");
|
|
4033
|
-
program.command("review").description("Run AI-powered review on a pull request").option("--sonar-url <url>", "SonarQube/SonarCloud base URL (or IRA_SONAR_URL)").option("--sonar-token <token>", "SonarQube API token (or IRA_SONAR_TOKEN)").option("--project-key <key>", "Sonar project key (or IRA_PROJECT_KEY)").option("--pr <id>", "Pull request ID (or IRA_PR)").option("--scm-provider <provider>", "SCM provider: bitbucket or github (or IRA_SCM_PROVIDER)").option("--bitbucket-token <token>", "Bitbucket API token (or IRA_BITBUCKET_TOKEN)").option("--repo <repo>", "Bitbucket workspace/repo-slug (Cloud) or PROJECT/repo-slug (Server) (or IRA_REPO)").option("--bitbucket-url <url>", "Bitbucket base URL (or IRA_BITBUCKET_URL)").option("--bitbucket-type <type>", "Bitbucket type: cloud or server (auto-detects from URL if omitted; or IRA_BITBUCKET_TYPE)").option("--github-token <token>", "GitHub API token (or IRA_GITHUB_TOKEN)").option("--github-repo <repo>", "GitHub owner/repo (or IRA_GITHUB_REPO)").option("--github-url <url>", "GitHub Enterprise URL (or IRA_GITHUB_URL)").option("--ai-provider <provider>", "AI provider").option("--ai-model <model>", "AI model to use").option("--dry-run", "Print comments to stdout instead of posting to SCM").option("--min-severity <level>", "Minimum severity to review (BLOCKER|CRITICAL|MAJOR|MINOR|INFO)").option("--jira-url <url>", "JIRA base URL (or IRA_JIRA_URL)").option("--jira-email <email>", "JIRA email (or IRA_JIRA_EMAIL)").option("--jira-token <token>", "JIRA API token (or IRA_JIRA_TOKEN)").option("--jira-type <type>", "JIRA type: cloud or server (auto-detects from URL if omitted)").option("--jira-ticket <key>", "JIRA ticket key (e.g. PROJ-123)").option("--jira-ac-field <field>", "Custom field ID for acceptance criteria").option("--jira-ac-source <source>", "Where to look for AC: customField, description, or both (default: customField)").option("--slack-webhook <url>", "Slack webhook URL for notifications").option("--teams-webhook <url>", "Teams webhook URL for notifications").option("--notify-min-risk <level>", "Only notify when risk is at or above this level: low, medium, high, critical").option("--notify-on-ac-fail", "Send notification when JIRA acceptance criteria validation fails").option("--ai-base-url <url>", "AI provider base URL (Azure endpoint, Ollama URL)").option("--ai-api-key <key>", "AI API key (or IRA_AI_API_KEY / OPENAI_API_KEY)").option("--ai-api-version <version>", "Azure OpenAI API version").option("--ai-deployment <name>", "Azure OpenAI deployment name").option("--ai-model-critical <model>", "Stronger AI model for BLOCKER/CRITICAL issues").option("--generate-tests", "Generate test cases from JIRA acceptance criteria").option("--test-framework <framework>", "Test framework: jest, vitest, mocha, playwright, cypress, gherkin, pytest, junit (default: jest)").option("--config <path>", "Path to config file (default: auto-detect .irarc.json / ira.config.json)").option("--no-config-file", "Disable auto-loading config file from repo").option("--rules-url <url>", "Fetch .ira-rules.json from URL (raw HTTP) \u2014 useful when CI has no full checkout (or IRA_RULES_URL)").option("--comment-style <style>", "Comment formatter style: compact (default) or detailed (or IRA_COMMENT_STYLE)").action(async (opts) => {
|
|
4083
|
+
program.command("review").description("Run AI-powered review on a pull request").option("--sonar-url <url>", "SonarQube/SonarCloud base URL (or IRA_SONAR_URL)").option("--sonar-token <token>", "SonarQube API token (or IRA_SONAR_TOKEN)").option("--project-key <key>", "Sonar project key (or IRA_PROJECT_KEY)").option("--pr <id>", "Pull request ID (or IRA_PR)").option("--scm-provider <provider>", "SCM provider: bitbucket or github (or IRA_SCM_PROVIDER)").option("--bitbucket-token <token>", "Bitbucket API token (or IRA_BITBUCKET_TOKEN)").option("--repo <repo>", "Bitbucket workspace/repo-slug (Cloud) or PROJECT/repo-slug (Server) (or IRA_REPO)").option("--bitbucket-url <url>", "Bitbucket base URL (or IRA_BITBUCKET_URL)").option("--bitbucket-type <type>", "Bitbucket type: cloud or server (auto-detects from URL if omitted; or IRA_BITBUCKET_TYPE)").option("--github-token <token>", "GitHub API token (or IRA_GITHUB_TOKEN)").option("--github-repo <repo>", "GitHub owner/repo (or IRA_GITHUB_REPO)").option("--github-url <url>", "GitHub Enterprise URL (or IRA_GITHUB_URL)").option("--ai-provider <provider>", "AI provider").option("--ai-model <model>", "AI model to use").option("--dry-run", "Print comments to stdout instead of posting to SCM").option("--min-severity <level>", "Minimum severity to review (BLOCKER|CRITICAL|MAJOR|MINOR|INFO)").option("--jira-url <url>", "JIRA base URL (or IRA_JIRA_URL)").option("--jira-email <email>", "JIRA email (or IRA_JIRA_EMAIL)").option("--jira-token <token>", "JIRA API token (or IRA_JIRA_TOKEN)").option("--jira-type <type>", "JIRA type: cloud or server (auto-detects from URL if omitted)").option("--jira-ticket <key>", "JIRA ticket key (e.g. PROJ-123)").option("--jira-ac-field <field>", "Custom field ID for acceptance criteria").option("--jira-ac-source <source>", "Where to look for AC: customField, description, or both (default: customField)").option("--no-post-acs-to-jira", "Don't post AI-generated acceptance criteria back to the JIRA ticket when none exist; keep them in the PR summary only (or IRA_POST_ACS_TO_JIRA=false)").option("--slack-webhook <url>", "Slack webhook URL for notifications").option("--teams-webhook <url>", "Teams webhook URL for notifications").option("--notify-min-risk <level>", "Only notify when risk is at or above this level: low, medium, high, critical").option("--notify-on-ac-fail", "Send notification when JIRA acceptance criteria validation fails").option("--ai-base-url <url>", "AI provider base URL (Azure endpoint, Ollama URL)").option("--ai-api-key <key>", "AI API key (or IRA_AI_API_KEY / OPENAI_API_KEY)").option("--ai-api-version <version>", "Azure OpenAI API version").option("--ai-deployment <name>", "Azure OpenAI deployment name").option("--ai-model-critical <model>", "Stronger AI model for BLOCKER/CRITICAL issues").option("--generate-tests", "Generate test cases from JIRA acceptance criteria").option("--test-framework <framework>", "Test framework: jest, vitest, mocha, playwright, cypress, gherkin, pytest, junit (default: jest)").option("--config <path>", "Path to config file (default: auto-detect .irarc.json / ira.config.json)").option("--no-config-file", "Disable auto-loading config file from repo").option("--rules-url <url>", "Fetch .ira-rules.json from URL (raw HTTP) \u2014 useful when CI has no full checkout (or IRA_RULES_URL)").option("--comment-style <style>", "Comment formatter style: compact (default) or detailed (or IRA_COMMENT_STYLE)").action(async (opts) => {
|
|
4034
4084
|
try {
|
|
4035
4085
|
console.log(`
|
|
4036
4086
|
\u{1F50D} IRA \u2014 Scanning PR before your reviewers do
|
|
@@ -4065,6 +4115,11 @@ program.command("review").description("Run AI-powered review on a pull request")
|
|
|
4065
4115
|
...opts.jiraTicket && { jiraTicket: opts.jiraTicket.toUpperCase() },
|
|
4066
4116
|
...opts.jiraAcField && { jiraAcField: opts.jiraAcField },
|
|
4067
4117
|
...opts.jiraAcSource && { jiraAcSource: opts.jiraAcSource },
|
|
4118
|
+
// commander's --no-X pattern → opts.postAcsToJira is `true` by default,
|
|
4119
|
+
// `false` only when the user passed `--no-post-acs-to-jira`. Only push
|
|
4120
|
+
// through the explicit-disable case; otherwise leave undefined so env
|
|
4121
|
+
// var (or the engine's built-in default of "enabled") wins.
|
|
4122
|
+
...opts.postAcsToJira === false && { postAcsToJira: false },
|
|
4068
4123
|
...opts.slackWebhook && { slackWebhook: opts.slackWebhook },
|
|
4069
4124
|
...opts.teamsWebhook && { teamsWebhook: opts.teamsWebhook },
|
|
4070
4125
|
...opts.notifyMinRisk && { notifyMinRisk: opts.notifyMinRisk },
|
package/dist/index.cjs
CHANGED
|
@@ -1796,10 +1796,17 @@ var BitbucketServerClient = class {
|
|
|
1796
1796
|
});
|
|
1797
1797
|
}
|
|
1798
1798
|
async getIssueComments(pullRequestId) {
|
|
1799
|
+
const collect = (node, sink) => {
|
|
1800
|
+
if (!node) return;
|
|
1801
|
+
if (typeof node.text === "string" && node.text.length > 0) sink.push(node.text);
|
|
1802
|
+
if (Array.isArray(node.comments)) {
|
|
1803
|
+
for (const child of node.comments) collect(child, sink);
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1799
1806
|
const bodies = [];
|
|
1800
1807
|
let start = 0;
|
|
1801
1808
|
while (true) {
|
|
1802
|
-
const url = this.prUrl(pullRequestId, `/
|
|
1809
|
+
const url = this.prUrl(pullRequestId, `/activities?start=${start}&limit=100`);
|
|
1803
1810
|
const data = await withRetry(async () => {
|
|
1804
1811
|
const response = await fetchWithTimeout(url, { headers: this.headers });
|
|
1805
1812
|
if (!response.ok) {
|
|
@@ -1811,7 +1818,9 @@ var BitbucketServerClient = class {
|
|
|
1811
1818
|
}
|
|
1812
1819
|
return await response.json();
|
|
1813
1820
|
});
|
|
1814
|
-
for (const
|
|
1821
|
+
for (const activity of data.values) {
|
|
1822
|
+
if (activity.action === "COMMENTED") collect(activity.comment, bodies);
|
|
1823
|
+
}
|
|
1815
1824
|
if (data.isLastPage) break;
|
|
1816
1825
|
start = data.nextPageStart ?? start + 100;
|
|
1817
1826
|
}
|
|
@@ -3411,6 +3420,29 @@ function buildSummary2(result) {
|
|
|
3411
3420
|
lines.push("");
|
|
3412
3421
|
}
|
|
3413
3422
|
}
|
|
3423
|
+
const acGapExists = result.requirementCompletion && result.requirementCompletion.completionPercentage < 100 || result.acceptanceValidation && !result.acceptanceValidation.overallPass;
|
|
3424
|
+
if (result.comments.length === 0 && !acGapExists) {
|
|
3425
|
+
const fw = result.framework ?? "your stack";
|
|
3426
|
+
const acLine = result.requirementCompletion ? `Acceptance criteria for **${result.requirementCompletion.jiraKey}**: **${result.requirementCompletion.completionPercentage}% covered** (${result.requirementCompletion.metCriteria}/${result.requirementCompletion.totalCriteria}).` : result.acceptanceValidation ? `Acceptance criteria for **${result.acceptanceValidation.jiraKey}**: ${result.acceptanceValidation.overallPass ? "**all met** \u2705" : "**partially met** \u2014 see the JIRA section above"}.` : result.acGeneration && result.acGeneration.criteria.length > 0 ? result.acGeneration.postedToJira ? `\u{1F4DD} No acceptance criteria found on **${result.acGeneration.jiraKey}** \u2014 IRA generated **${result.acGeneration.totalCriteria} suggested AC${result.acGeneration.totalCriteria === 1 ? "" : "s"}** and posted them as a comment on the JIRA ticket for the Product Owner / requirement author to review and refine.` : `\u{1F4DD} No acceptance criteria found on **${result.acGeneration.jiraKey}** \u2014 IRA generated **${result.acGeneration.totalCriteria} suggested AC${result.acGeneration.totalCriteria === 1 ? "" : "s"}** (see the Suggested Acceptance Criteria section below).` : null;
|
|
3427
|
+
lines.push("## \u2705 All Clear \u2014 No Issues Found");
|
|
3428
|
+
lines.push("");
|
|
3429
|
+
lines.push(`> \u{1F389} **Nice work on PR #${result.pullRequestId}!**`);
|
|
3430
|
+
lines.push(`>`);
|
|
3431
|
+
lines.push(`> IRA scanned every changed file across **${fw}** and didn't surface a single concern.`);
|
|
3432
|
+
if (acLine) {
|
|
3433
|
+
lines.push(`>`);
|
|
3434
|
+
lines.push(`> ${acLine}`);
|
|
3435
|
+
}
|
|
3436
|
+
if (result.risk) {
|
|
3437
|
+
lines.push(`>`);
|
|
3438
|
+
lines.push(`> Risk score: **${result.risk.score}/${result.risk.maxScore}** (${result.risk.level}).`);
|
|
3439
|
+
}
|
|
3440
|
+
lines.push(`>`);
|
|
3441
|
+
lines.push(`> \u2705 **Safe to approve from an automated-review standpoint.**`);
|
|
3442
|
+
lines.push(`>`);
|
|
3443
|
+
lines.push(`> \u{1F465} **Human reviewer approval is still required before merge.** IRA augments your code review process \u2014 it doesn't replace it. Please ensure your team's review and approval requirements have been met before merging.`);
|
|
3444
|
+
lines.push("");
|
|
3445
|
+
}
|
|
3414
3446
|
lines.push("## Overview");
|
|
3415
3447
|
lines.push("");
|
|
3416
3448
|
lines.push(`| Metric | Value |`);
|
|
@@ -3658,7 +3690,10 @@ var Notifier = class {
|
|
|
3658
3690
|
var import_node_child_process2 = require("child_process");
|
|
3659
3691
|
function resolveGitRoot() {
|
|
3660
3692
|
try {
|
|
3661
|
-
return (0, import_node_child_process2.execSync)("git rev-parse --show-toplevel", {
|
|
3693
|
+
return (0, import_node_child_process2.execSync)("git rev-parse --show-toplevel", {
|
|
3694
|
+
encoding: "utf-8",
|
|
3695
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3696
|
+
}).trim();
|
|
3662
3697
|
} catch {
|
|
3663
3698
|
return process.cwd();
|
|
3664
3699
|
}
|
|
@@ -4217,7 +4252,11 @@ Cause: ${warnings[warnings.length - 1]}` : "";
|
|
|
4217
4252
|
let commitMessages = [];
|
|
4218
4253
|
try {
|
|
4219
4254
|
const { execSync: execSync4 } = await import("child_process");
|
|
4220
|
-
const gitLog = execSync4("git log --oneline -20 --no-decorate", {
|
|
4255
|
+
const gitLog = execSync4("git log --oneline -20 --no-decorate", {
|
|
4256
|
+
cwd: repoPath,
|
|
4257
|
+
encoding: "utf-8",
|
|
4258
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4259
|
+
});
|
|
4221
4260
|
commitMessages = gitLog.trim().split("\n").filter(Boolean);
|
|
4222
4261
|
} catch {
|
|
4223
4262
|
}
|
|
@@ -4236,15 +4275,23 @@ Cause: ${warnings[warnings.length - 1]}` : "";
|
|
|
4236
4275
|
if (acGeneration.parseWarning) {
|
|
4237
4276
|
warnings.push(acGeneration.parseWarning);
|
|
4238
4277
|
}
|
|
4239
|
-
|
|
4278
|
+
const postingDisabled = this.config.postAcsToJira === false;
|
|
4279
|
+
if (acGeneration.criteria.length > 0 && !this.config.dryRun && !postingDisabled) {
|
|
4240
4280
|
try {
|
|
4241
4281
|
const commentBody = formatACsForJiraComment(acGeneration, pullRequestId);
|
|
4242
4282
|
await jiraClient.addComment(this.config.jiraTicket, commentBody);
|
|
4243
4283
|
console.log(` Posted ${acGeneration.totalCriteria} suggested ACs to ${this.config.jiraTicket}`);
|
|
4284
|
+
acGeneration.postedToJira = true;
|
|
4244
4285
|
} catch (error) {
|
|
4245
4286
|
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
4246
4287
|
warnings.push(`Failed to post AC suggestions to JIRA: ${msg}`);
|
|
4288
|
+
acGeneration.postedToJira = false;
|
|
4247
4289
|
}
|
|
4290
|
+
} else if (postingDisabled && acGeneration.criteria.length > 0) {
|
|
4291
|
+
console.log(` Skipped posting ${acGeneration.totalCriteria} suggested ACs to ${this.config.jiraTicket} (--no-post-acs-to-jira). See PR summary.`);
|
|
4292
|
+
acGeneration.postedToJira = false;
|
|
4293
|
+
} else {
|
|
4294
|
+
acGeneration.postedToJira = false;
|
|
4248
4295
|
}
|
|
4249
4296
|
}
|
|
4250
4297
|
}
|
|
@@ -4458,6 +4505,8 @@ function resolveConfigFromEnv(overrides = {}) {
|
|
|
4458
4505
|
}
|
|
4459
4506
|
const jiraAcSource = overrides.jiraAcSource ?? optionalEnv("IRA_JIRA_AC_SOURCE");
|
|
4460
4507
|
const jiraTicket = overrides.jiraTicket ?? optionalEnv("IRA_JIRA_TICKET");
|
|
4508
|
+
const postAcsToJiraEnv = optionalEnv("IRA_POST_ACS_TO_JIRA");
|
|
4509
|
+
const postAcsToJira = overrides.postAcsToJira === false ? false : postAcsToJiraEnv && /^(false|0|no|off)$/i.test(postAcsToJiraEnv) ? false : void 0;
|
|
4461
4510
|
const commentStyleRaw = overrides.commentStyle ?? optionalEnv("IRA_COMMENT_STYLE");
|
|
4462
4511
|
if (commentStyleRaw && commentStyleRaw !== "compact" && commentStyleRaw !== "detailed") {
|
|
4463
4512
|
throw new Error(`Invalid comment-style: "${commentStyleRaw}". Must be "compact" or "detailed".`);
|
|
@@ -4491,6 +4540,7 @@ function resolveConfigFromEnv(overrides = {}) {
|
|
|
4491
4540
|
...overrides.generateTests && { generateTests: overrides.generateTests },
|
|
4492
4541
|
...overrides.testFramework && { testFramework: overrides.testFramework },
|
|
4493
4542
|
...jiraAcSource && { jiraAcSource },
|
|
4543
|
+
...postAcsToJira === false && { postAcsToJira: false },
|
|
4494
4544
|
...commentStyle && { commentStyle },
|
|
4495
4545
|
...rulesUrl && { rulesUrl }
|
|
4496
4546
|
};
|