ira-review 3.1.1 → 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 +52 -4
- package/dist/index.cjs +46 -3
- 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 +46 -3
- 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 |`);
|
|
@@ -3429,15 +3461,23 @@ Cause: ${warnings[warnings.length - 1]}` : "";
|
|
|
3429
3461
|
if (acGeneration.parseWarning) {
|
|
3430
3462
|
warnings.push(acGeneration.parseWarning);
|
|
3431
3463
|
}
|
|
3432
|
-
|
|
3464
|
+
const postingDisabled = this.config.postAcsToJira === false;
|
|
3465
|
+
if (acGeneration.criteria.length > 0 && !this.config.dryRun && !postingDisabled) {
|
|
3433
3466
|
try {
|
|
3434
3467
|
const commentBody = formatACsForJiraComment(acGeneration, pullRequestId);
|
|
3435
3468
|
await jiraClient.addComment(this.config.jiraTicket, commentBody);
|
|
3436
3469
|
console.log(` Posted ${acGeneration.totalCriteria} suggested ACs to ${this.config.jiraTicket}`);
|
|
3470
|
+
acGeneration.postedToJira = true;
|
|
3437
3471
|
} catch (error) {
|
|
3438
3472
|
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
3439
3473
|
warnings.push(`Failed to post AC suggestions to JIRA: ${msg}`);
|
|
3474
|
+
acGeneration.postedToJira = false;
|
|
3440
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;
|
|
3441
3481
|
}
|
|
3442
3482
|
}
|
|
3443
3483
|
}
|
|
@@ -3651,6 +3691,8 @@ function resolveConfigFromEnv(overrides = {}) {
|
|
|
3651
3691
|
}
|
|
3652
3692
|
const jiraAcSource = overrides.jiraAcSource ?? optionalEnv("IRA_JIRA_AC_SOURCE");
|
|
3653
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;
|
|
3654
3696
|
const commentStyleRaw = overrides.commentStyle ?? optionalEnv("IRA_COMMENT_STYLE");
|
|
3655
3697
|
if (commentStyleRaw && commentStyleRaw !== "compact" && commentStyleRaw !== "detailed") {
|
|
3656
3698
|
throw new Error(`Invalid comment-style: "${commentStyleRaw}". Must be "compact" or "detailed".`);
|
|
@@ -3684,6 +3726,7 @@ function resolveConfigFromEnv(overrides = {}) {
|
|
|
3684
3726
|
...overrides.generateTests && { generateTests: overrides.generateTests },
|
|
3685
3727
|
...overrides.testFramework && { testFramework: overrides.testFramework },
|
|
3686
3728
|
...jiraAcSource && { jiraAcSource },
|
|
3729
|
+
...postAcsToJira === false && { postAcsToJira: false },
|
|
3687
3730
|
...commentStyle && { commentStyle },
|
|
3688
3731
|
...rulesUrl && { rulesUrl }
|
|
3689
3732
|
};
|
|
@@ -4037,7 +4080,7 @@ function logEnvironmentInfo(config) {
|
|
|
4037
4080
|
}
|
|
4038
4081
|
var program = new Command();
|
|
4039
4082
|
program.name("ira-review").description("AI-powered PR review tool with SonarQube + GitHub/Bitbucket integration").version("3.1.0");
|
|
4040
|
-
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) => {
|
|
4041
4084
|
try {
|
|
4042
4085
|
console.log(`
|
|
4043
4086
|
\u{1F50D} IRA \u2014 Scanning PR before your reviewers do
|
|
@@ -4072,6 +4115,11 @@ program.command("review").description("Run AI-powered review on a pull request")
|
|
|
4072
4115
|
...opts.jiraTicket && { jiraTicket: opts.jiraTicket.toUpperCase() },
|
|
4073
4116
|
...opts.jiraAcField && { jiraAcField: opts.jiraAcField },
|
|
4074
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 },
|
|
4075
4123
|
...opts.slackWebhook && { slackWebhook: opts.slackWebhook },
|
|
4076
4124
|
...opts.teamsWebhook && { teamsWebhook: opts.teamsWebhook },
|
|
4077
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 |`);
|
|
@@ -4243,15 +4275,23 @@ Cause: ${warnings[warnings.length - 1]}` : "";
|
|
|
4243
4275
|
if (acGeneration.parseWarning) {
|
|
4244
4276
|
warnings.push(acGeneration.parseWarning);
|
|
4245
4277
|
}
|
|
4246
|
-
|
|
4278
|
+
const postingDisabled = this.config.postAcsToJira === false;
|
|
4279
|
+
if (acGeneration.criteria.length > 0 && !this.config.dryRun && !postingDisabled) {
|
|
4247
4280
|
try {
|
|
4248
4281
|
const commentBody = formatACsForJiraComment(acGeneration, pullRequestId);
|
|
4249
4282
|
await jiraClient.addComment(this.config.jiraTicket, commentBody);
|
|
4250
4283
|
console.log(` Posted ${acGeneration.totalCriteria} suggested ACs to ${this.config.jiraTicket}`);
|
|
4284
|
+
acGeneration.postedToJira = true;
|
|
4251
4285
|
} catch (error) {
|
|
4252
4286
|
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
4253
4287
|
warnings.push(`Failed to post AC suggestions to JIRA: ${msg}`);
|
|
4288
|
+
acGeneration.postedToJira = false;
|
|
4254
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;
|
|
4255
4295
|
}
|
|
4256
4296
|
}
|
|
4257
4297
|
}
|
|
@@ -4465,6 +4505,8 @@ function resolveConfigFromEnv(overrides = {}) {
|
|
|
4465
4505
|
}
|
|
4466
4506
|
const jiraAcSource = overrides.jiraAcSource ?? optionalEnv("IRA_JIRA_AC_SOURCE");
|
|
4467
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;
|
|
4468
4510
|
const commentStyleRaw = overrides.commentStyle ?? optionalEnv("IRA_COMMENT_STYLE");
|
|
4469
4511
|
if (commentStyleRaw && commentStyleRaw !== "compact" && commentStyleRaw !== "detailed") {
|
|
4470
4512
|
throw new Error(`Invalid comment-style: "${commentStyleRaw}". Must be "compact" or "detailed".`);
|
|
@@ -4498,6 +4540,7 @@ function resolveConfigFromEnv(overrides = {}) {
|
|
|
4498
4540
|
...overrides.generateTests && { generateTests: overrides.generateTests },
|
|
4499
4541
|
...overrides.testFramework && { testFramework: overrides.testFramework },
|
|
4500
4542
|
...jiraAcSource && { jiraAcSource },
|
|
4543
|
+
...postAcsToJira === false && { postAcsToJira: false },
|
|
4501
4544
|
...commentStyle && { commentStyle },
|
|
4502
4545
|
...rulesUrl && { rulesUrl }
|
|
4503
4546
|
};
|