Skip to content

fix(triage): resolve Jira completion transitions dynamically with Done fallback - #1998

Open
jefferyb wants to merge 6 commits into
fullsend-ai:mainfrom
jefferyb:fix/triage-jira-completed-transition
Open

jefferyb wants to merge 6 commits into
fullsend-ai:mainfrom
jefferyb:fix/triage-jira-completed-transition

Conversation

@jefferyb

@jefferyb jefferyb commented Nov 13, 2026 •

Copy link
Copy Markdown

Summary

When the triage agent marks an issue with action: completed, post-triage.sh previously reused JIRA_SPLIT_TRANSITION (defaulting to "Obsolete" per ADR 0080/0081) to transition the issue in Jira Cloud.

In standard Jira Cloud and Team-Managed software projects, "Obsolete" is not part of the active workflow. When the transition name is missing from the issue's available transitions, tracker_close_issue fails and post-triage.sh terminates with exit status 1 under set -e.

This PR:

  1. Decouples action: split from action: completed in scripts/post-triage.src.sh, mapping split to GitHub reason "completed" in github-triage-ops.lib.sh while allowing JIRA_COMPLETED_TRANSITION in Jira (falling back through JIRA_SPLIT_TRANSITION when unset so existing overlays and custom workflows are preserved).
  2. Implements dynamic transition resolution and idempotency in scripts/lib/jira-triage-ops.lib.sh:
    • For completed and split, skips idempotently if the current Jira status already matches the configured transition name (or its .to.name target status on available transitions) or is a positive statusCategory == "done" status (excluding rejection statuses unless matched intentionally).
    • For duplicate and not planned, skips idempotently only when the current status matches the configured transition name or its .to.name target status.
    • Falls back safely on completed/split (emitting a ::warning:: annotation when substituting) to available to.statusCategory.key == "done" transitions whose target status (.to.name // .name) matches positive Done names (preferring exact matches for "Done", "Closed", "Resolved", "Complete", "Completed" before word-boundary matches), while excluding rejection names on both .name and .to.name.
  3. Rebuilds script bundles via make script-build.

Related: fullsend-ai/fullsend#6704

Changes

  • scripts/post-triage.src.sh: Updated action: split to pass "split" instead of "completed" to tracker_close_issue.
  • scripts/lib/github-triage-ops.lib.sh: Mapped close reason "split" to GitHub state reason "completed".
  • scripts/lib/jira-triage-ops.lib.sh:
    • Added optional JIRA_COMPLETED_TRANSITION override (no default in harness/triage.yaml; falls back to JIRA_SPLIT_TRANSITION when unset).
    • Added idempotency checks for completed/split and exact target-status idempotency for duplicate/not planned.
    • Added ranked to.statusCategory.key == "done" fallback matching (.to.name // .name) with ::warning:: annotation and rejection exclusions on both .name and .to.name.
  • docs/triage.md & LOCAL.md: Documented JIRA_COMPLETED_TRANSITION and fallback/idempotency behavior.
  • scripts/post-triage-test.sh: Added unit tests using mocked Jira REST API responses in post-triage-test.sh (not tested against a live Jira Cloud project).
  • Rebuilt bundles: scripts/post-triage.sh, scripts/pre-code-jira.sh, scripts/pre-triage.sh.

Testing

  • make script-build and make check-bundle pass cleanly.
  • shellcheck --severity=warning scripts/lib/jira-triage-ops.lib.sh scripts/post-triage.src.sh scripts/post-triage.sh scripts/pre-code-jira.sh scripts/pre-triage.sh scripts/post-triage-test.sh passes cleanly.
  • SCRIPT_TEST_TARGET=source bash scripts/post-triage-test.sh and SCRIPT_TEST_TARGET=bundled bash scripts/post-triage-test.sh pass all tests (mocked Jira workflow responses).

…ns dynamically

When the triage agent marks an issue with action: completed, post-triage.sh
currently reuses JIRA_SPLIT_TRANSITION (defaulting to "Obsolete" per ADR 0080/0081)
to transition the issue in Jira Cloud. In workflows where "Obsolete" is not
present, tracker_close_issue fails and post-triage.sh exits with status 1
under set -e.

- Add JIRA_COMPLETED_TRANSITION (defaulting to "Closed") in harness/triage.yaml.
- Decouple action: split from action: completed in scripts/post-triage.src.sh,
  mapping split to GitHub reason "completed" in github-triage-ops.lib.sh.
- Add dynamic transition resolution and idempotency check in jira-triage-ops.lib.sh.
- Rebuild script bundles via make script-build.
- Document JIRA_COMPLETED_TRANSITION in docs/triage.md and LOCAL.md.
- Add unit tests for completed transition, fallback, and idempotency in post-triage-test.sh.

Signed-off-by: Jeffery Bagirimvano <jefferyb@redhat.com>
@jefferyb
jefferyb requested a review from a team as a code owner November 13, 2026 15:37
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Decouple Jira completed and split transitions with safe fallback

🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Give completed and split actions separate Jira transitions while preserving GitHub’s completed
 close reason.
• Resolve unavailable Jira transitions through safe Done-category fallbacks and skip issues already
 Done.
• Document the new setting, cover transition behavior in tests, and rebuild script bundles.
Diagram

graph TD
  A["Triage close actions"] --> B["Jira adapter"] --> C{"Already Done?"}
  C -->|No| D{"Configured available?"} -->|No| E["Safe Done fallback"] --> F["Jira transitions"]
  C -->|Yes| H["Skip transition"]
  D -->|Yes| F
  A --> G["GitHub close"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Require exact configured transitions
  • ➕ Never selects a transition other than the configured one.
  • ➖ Retains the failure when that transition is absent from an issue's workflow.
2. Choose any Done-category transition
  • ➕ Requires less per-workflow configuration.
  • ➖ Could select a rejection transition and lose the distinction between completed and split.

Recommendation: Keep separate configured names with an exact-match-first, restricted fallback. This preserves workflow intent while handling common Jira transition differences; the Done check also makes repeat closes safe.

Files changed (10) +190 / -13

Bug fix (6) +147 / -6
github-triage-ops.lib.shPreserve GitHub’s completed reason for splits +3/-0

Preserve GitHub’s completed reason for splits

• Translates the new split close reason to completed before calling GitHub.

scripts/lib/github-triage-ops.lib.sh

jira-triage-ops.lib.shResolve Jira close transitions by action and workflow +34/-1

Resolve Jira close transitions by action and workflow

• Maps completed and split to distinct settings, skips issues already in the Done category, and falls back to a non-rejection Done-category transition when the configured name is absent.

scripts/lib/jira-triage-ops.lib.sh

post-triage.shRebuild the executable post-triage bundle +38/-2

Rebuild the executable post-triage bundle

• Incorporates the split close reason and updated GitHub and Jira tracker adapters into the generated script.

scripts/post-triage.sh

post-triage.src.shPass split as its own close reason +1/-1

Pass split as its own close reason

• Dispatches split to tracker_close_issue as split rather than completed, allowing tracker-specific handling.

scripts/post-triage.src.sh

pre-code-jira.shRebuild the pre-code Jira bundle +34/-1

Rebuild the pre-code Jira bundle

• Carries the shared Jira close-action mapping, Done check, and fallback resolution into the generated script.

scripts/pre-code-jira.sh

pre-triage.shRebuild the pre-triage bundle +37/-1

Rebuild the pre-triage bundle

• Carries the updated GitHub split mapping and Jira close-transition resolution into the generated script.

scripts/pre-triage.sh

Tests (1) +34 / -5
post-triage-test.shCover Jira completed, fallback, and repeat-close behavior +34/-5

Cover Jira completed, fallback, and repeat-close behavior

• Extends Jira mocks with status and destination categories. Tests the completed transition, fallback selection, already-Done skip, and distinct split transition.

scripts/post-triage-test.sh

Documentation (2) +8 / -2
LOCAL.mdShow the completed-transition environment setting +1/-0

Show the completed-transition environment setting

• Adds JIRA_COMPLETED_TRANSITION to the local Jira configuration example.

LOCAL.md

triage.mdDocument distinct Jira close transitions and fallback +7/-2

Document distinct Jira close transitions and fallback

• Assigns completed and split to separate settings and explains the Done-category fallback when a configured transition is unavailable.

docs/triage.md

Other (1) +1 / -0
triage.yamlDefault completed Jira transitions to Closed +1/-0

Default completed Jira transitions to Closed

• Adds JIRA_COMPLETED_TRANSITION with a Closed default without changing the split transition default.

harness/triage.yaml

@qodo-code-review

qodo-code-review Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Local Jira runs ignore the new export ✓ Resolved

Resolving commit: e459281

Resolution reason
LOCAL.md no longer exports a conflicting JIRA_COMPLETED_TRANSITION="Done"; it now states the harness
default ("Closed") and explains how to override it in a harness overlay or by exporting when
invoking post-triage.sh directly, keeping the documentation consistent with what the runner passes.
Description
LOCAL.md tells operators to export JIRA_COMPLETED_TRANSITION="Done", but the Jira harness sets
its runner environment value to the literal "Closed". A local run using that harness therefore
supplies Closed to the post-script, so a workflow exposing both transitions will use Closed
despite the operator's exported choice.

2. Header misleads on Jira transition setup ✓ Resolved

Resolving commit: 1ac67d3

Resolution reason
The header in scripts/lib/jira-triage-ops.lib.sh was rewritten to add a JIRA_COMPLETED_TRANSITION
entry (default "Closed", falling back to JIRA_SPLIT_TRANSITION) and to document
JIRA_SPLIT_TRANSITION only for the split action. The same corrected header was propagated to the
rebuilt post-triage.sh, pre-code-jira.sh and pre-triage.sh bundles. The suggested fix's "make
script-build" step is not shown, but the bundles were updated directly with identical text. The fix
is in the header, which is what the finding flagged.
Description
The env-var header in scripts/lib/jira-triage-ops.lib.sh says JIRA_SPLIT_TRANSITION covers both
split and completed and omits JIRA_COMPLETED_TRANSITION, even though the action mapping now
uses the latter for completed. Operators configuring Jira from that header may leave
JIRA_COMPLETED_TRANSITION unset, causing completed to fail with an unset-variable error; the
same stale guidance also appears in the rebuilt scripts/post-triage.sh, scripts/pre-code-jira.sh
and scripts/pre-triage.sh bundles.

3. Completed issues can be closed as rejected ✓ Resolved

Resolving commit: 1ac67d3

Resolution reason
The fallback jq filter now explicitly excludes the configured JIRA_DUPLICATE_TRANSITION and
JIRA_NOT_PLANNED_TRANSITION names via --arg, extends the denylist with not.?planned|invalid|cannot
reproduce|abandon, and removes the $done_transitions[0].id last resort so only names matching
^(done|closed|resolved|complete(d)?)$ are accepted. The same change is applied in
jira-triage-ops.lib.sh, post-triage.sh, pre-code-jira.sh, and pre-triage.sh, and the idempotency
check was also made rejection-aware. Tests were added where Not Planned/Duplicate-only transitions
are refused.
Description
tracker_close_issue falls back to the first Done-category transition whose name escapes a short
rejection denylist, including Not Planned, Invalid, Cannot Reproduce, and Abandoned. When
the configured close transition is unavailable and no transition is named Done, Closed, or Resolved,
a completed or split action can select Not Planned, which this repository configures
separately for the not-planned action.

View medium (2)
4. Done issues retain the wrong resolution ✓ Resolved

Resolving commit: 1ac67d3

Resolution reason
The idempotency skip for a Done-category issue now only applies when the current status name is not
a rejection status for completed/split actions, so a completed action on a Won't Do/Duplicate issue
proceeds to transition instead of reporting success. A regression test covering this case was added.
Description
tracker_close_issue returns success for every close reason whenever the current Jira status
category is done, without checking whether that status represents the requested outcome. If triage
selects completed for an issue already marked Duplicate or Won't Do, the function skips the
completion transition and reports that the action succeeded.

5. Existing Jira setups fail on completed action ✓ Resolved

Resolving commit: 1ac67d3

Resolution reason
The fallback is inserted before the unset-variable check in tracker_close_issue: when reason is
"completed" and JIRA_COMPLETED_TRANSITION is empty but JIRA_SPLIT_TRANSITION is set,
transition_var/transition_name are switched to JIRA_SPLIT_TRANSITION with a NOTICE logged. This is
exactly the suggested fix (fall back to JIRA_SPLIT_TRANSITION with a notice). The same change is
applied across the lib and all bundled copies (post-triage.sh, pre-code-jira.sh, pre-triage.sh), and
docs/triage.md is updated to describe the fallback. Done-category fallback also remains for when
neither is set.
Description
completed now reads JIRA_COMPLETED_TRANSITION, and the unset-variable check runs before the new
fallback, so setups that only define the original three variables hit `ERROR:
JIRA_COMPLETED_TRANSITION is not set and post-triage.sh exits under set -e`. Before this PR, the
same setups closed completed issues through JIRA_SPLIT_TRANSITION, so any environment configured
from the earlier LOCAL.md or its own runner env now breaks.

Grey Divider

Context sources
✅ Compliance rules (platform): 55 rules
✅ Skills: 4 invoked
  code-review
  code-implementation
  pr-review
  docs-review
✅ Cross-repo context — repo relationships
  Explored: repo: fullsend-ai/fullsend (sha: dda46236) — View relationship
  Explored: repo: fullsend-ai/pi-anthropic-vertex (sha: d2ecd039) — View relationship
  Explored: repo: fullsend-ai/pi-xai-vertex (sha: a86139bf) — View relationship
  Explored: repo: fullsend-ai/react-shopping-cart (sha: 5e9aa80b) — View relationship
  Explored: repo: fullsend-ai/.fullsend (sha: e6bfe5b1) — View relationship
  Explored: repo: fullsend-ai/devlake (sha: ba0670f2) — View relationship
Review mode: Auto: 🧠 Deep: Multiple Jira workflow paths and duplicated shell logic create subtle independent defect risks.

Grey Divider

Tip of the day
💡 Did you know, you can start a comment with 'qodo' or '@qodo' to chat about any finding

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread scripts/lib/jira-triage-ops.lib.sh Resolved
Comment thread scripts/lib/jira-triage-ops.lib.sh Resolved
Comment thread LOCAL.md Resolved
Comment thread scripts/lib/jira-triage-ops.lib.sh Resolved
Comment thread scripts/lib/jira-triage-ops.lib.sh Resolved
Signed-off-by: Jeffery Bagirimvano <jefferyb@redhat.com>
Signed-off-by: Jeffery Bagirimvano <jefferyb@redhat.com>

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review findings: 1 HIGH, 3 MEDIUM. Details are in the inline comments.

Comment thread harness/triage.yaml Outdated
# `fullsend issues` reads tracker.jira from this directory's config.
FULLSEND_DIR: ${FULLSEND_DIR}
JIRA_BASE_URL: ${JIRA_BASE_URL}
JIRA_COMPLETED_TRANSITION: "Closed"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH: Default for completed changes on upgrade, but the PR has no breaking-change marker

Before this PR, completed used JIRA_SPLIT_TRANSITION (harness default "Obsolete"). The harness now always sets JIRA_COMPLETED_TRANSITION: "Closed", so every managed Jira run picks a different transition. The documented NOTICE fallback to JIRA_SPLIT_TRANSITION (lib lines 368-381, docs/triage.md) can never fire through the shipped harness.

Two groups break on upgrade:

  • (a) Users whose overlay set only JIRA_SPLIT_TRANSITION to get their own completed transition now get "Closed" or a Done fallback instead, with no warning.
  • (b) Custom workflows that offer "Obsolete" but none of Done/Closed/Resolved/Complete used to succeed and now exit 1: the fallback regex at line 443 excludes "obsolete".

COMMITS.md:79 lists "Default values change in ways that alter existing behavior" as breaking, and COMMITS.md:115 asks reviewers to flag a missing ! as high severity. The title and all three commits are plain fix(triage): with no BREAKING CHANGE: trailer.

Suggestion: Retitle to fix(triage)!: and add a BREAKING CHANGE: trailer: completed now uses JIRA_COMPLETED_TRANSITION (harness default "Closed", with a Done-category fallback), and overlays that set only JIRA_SPLIT_TRANSITION must also set JIRA_COMPLETED_TRANSITION. Also fix docs/triage.md and the lib header so they no longer suggest the fallback to JIRA_SPLIT_TRANSITION applies under the harness. The alternative is to leave the harness value empty so the documented fallback really runs.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a274b35: removed JIRA_COMPLETED_TRANSITION: "Closed" from harness/triage.yaml (and updated LOCAL.md to match) so existing overlays and custom workflows preserve the documented fallback to JIRA_SPLIT_TRANSITION unless explicitly overridden.

Comment thread scripts/post-triage-test.sh Outdated
"" true

# Jira completed action falls back to JIRA_SPLIT_TRANSITION when JIRA_COMPLETED_TRANSITION is unset.
(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: Test jira-completed-fallback-to-split-var runs in a subshell, so its failures never reach the suite

Lines 2437-2442 call run_jira_test_stdout inside ( unset JIRA_COMPLETED_TRANSITION; ... ). The helper (line 2341) records a failure only with FAILURES=$((FAILURES + 1)) and returns 0. That increment is lost when the subshell exits, so the suite still prints All tests passed and exits 0. Changing the expected string so it cannot match confirms this: the run prints FAIL for this test and still exits 0. The JIRA_COMPLETED_TRANSITION -> JIRA_SPLIT_TRANSITION fallback therefore has no working regression test. The same file already handles env-changing cases correctly at lines 2967-2977 (JIRA_CASE_FAILURES file plus jira_case_fail).

Suggestion: Drop the subshell. Save JIRA_COMPLETED_TRANSITION, unset it, call the helper, then restore it. Or record the failure through the JIRA_CASE_FAILURES file mechanism. Then repeat the mutation check to confirm a broken expectation now fails the suite.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a274b35: replaced the subshell around jira-completed-fallback-to-split-var in scripts/post-triage-test.sh with save/restore of JIRA_COMPLETED_TRANSITION in the parent shell so FAILURES increments propagate to the suite.

Comment thread scripts/lib/jira-triage-ops.lib.sh Outdated
local is_rejection_status=0
local dup_name="${JIRA_DUPLICATE_TRANSITION:-}"
local not_planned_name="${JIRA_NOT_PLANNED_TRANSITION:-}"
if echo "${current_status_name}" | grep -qiE "won.?t do|not.?planned|duplicate|obsolete|reject|cancel|invalid|cannot reproduce|abandon"; then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: Re-running split on an issue already in "Obsolete" overwrites it to Done/Closed

The shipped split status is JIRA_SPLIT_TRANSITION="Obsolete", but "obsolete" is in the rejection regex in both the idempotency check (line 399) and the fallback exclusion (line 443). On a split re-run for an issue already in "Obsolete": is_rejection_status=1, so the split skip at lines 407-411 does not fire. The exact "Obsolete" transition is usually not offered from the Obsolete status, so the fallback picks Done/Closed and POSTs it. The issue moves out of its split resolution into a plain completed one. The split path has no tests for idempotency or fallback.

Suggestion: For reason=split, count the current status as handled when it matches the configured JIRA_SPLIT_TRANSITION name (case-insensitive), before the rejection regex is checked. More generally, skip when the current status matches the configured target for this reason. Add tests for split re-run on an Obsolete issue and for split when the Obsolete transition is unavailable.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a274b35: tracker_close_issue in scripts/lib/jira-triage-ops.lib.sh now checks case-insensitive equality against the target transition_name before evaluating the rejection regex, so re-running split when already in "Obsolete" skips idempotently. Added jira-split-idempotent-obsolete and jira-split-fallback-done tests.

Comment thread scripts/lib/jira-triage-ops.lib.sh Outdated
local not_planned_name="${JIRA_NOT_PLANNED_TRANSITION:-}"
if echo "${current_status_name}" | grep -qiE "won.?t do|not.?planned|duplicate|obsolete|reject|cancel|invalid|cannot reproduce|abandon"; then
is_rejection_status=1
elif [[ -n "${dup_name}" ]] && echo "${current_status_name}" | grep -qiF "${dup_name}"; then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: Configured transition names are matched as status substrings, which breaks the documented "set all four to the same name" setup

Lines 401-404 mark the current status as a rejection when its name contains JIRA_DUPLICATE_TRANSITION or JIRA_NOT_PLANNED_TRANSITION (grep -qiF, substring match). Those values are transition names, not status names. docs/triage.md tells users whose workflow does not tell outcomes apart to "Set all four to the same transition name", e.g. all "Done". With that setup, an issue already in status "Done" counts as a rejection, so the completed/split idempotent skip never fires. The exact "Done" transition is normally not offered from Done. The fallback (lines 438-441) then removes every transition whose name equals the duplicate/not-planned names, which is "Done" again. The run ends with ERROR: transition 'Done' ... is not available and exits 1, the failure this PR set out to remove, reached by following the docs.

Suggestion: Do not use the duplicate/not-planned names to classify status when they equal the name configured for the current reason. Better, compare the current status to the configured name for the requested reason first and skip on a match. Add a test with all four variables set to "Done" and the issue already in Done.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a274b35: dup_name and not_planned_name are now compared case-insensitively for equality only when they differ from transition_name, both in the pre-flight status check and in the fallback jq exclusion selector. Added jira-all-four-done-idempotent-completed covering all four transition vars set to "Done".

…tatus checks

Signed-off-by: Jeffery Bagirimvano <jefferyb@redhat.com>

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review findings: 4 MEDIUM. Details are in the inline comments.

# the raw REST calls in this file and the `fullsend` shell-outs below.
#
# Optional env vars:
# JIRA_COMPLETED_TRANSITION — transition name for the "completed" action

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: Header comment and PR body still say JIRA_COMPLETED_TRANSITION defaults to "Closed"; the shipped harness has no default

Commit a274b35 removed JIRA_COMPLETED_TRANSITION: "Closed" from harness/triage.yaml, and harness/triage.yaml is no longer in the diff. This header (lines 46-48) still says (default: "Closed"; falls back to JIRA_SPLIT_TRANSITION when unset), and the rebuilt bundles carry the same text (scripts/post-triage.sh, scripts/pre-code-jira.sh, scripts/pre-triage.sh). The PR body also still says it "Adds JIRA_COMPLETED_TRANSITION (defaulting to "Closed") in harness/triage.yaml" and lists triage.yaml under Changes.

With the shipped harness, completed falls back to JIRA_SPLIT_TRANSITION="Obsolete". So completed is decoupled from split only for operators who set the variable, and the reported failure is fixed by the name fallback, not by decoupling. LOCAL.md and docs/triage.md are correct; this header, which operators read first, is wrong. (The earlier header thread asked to add a "Closed" default; that default was later removed, so this is a new mismatch.)

Suggestion: Change the header to "no default; falls back to JIRA_SPLIT_TRANSITION when unset" and run make script-build so all four copies match. Update the PR title and body to say that, by default, completed still routes through JIRA_SPLIT_TRANSITION (with the Done-name fallback) and that harness/triage.yaml is unchanged.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 20bebaa (20bebaa): updated the JIRA_COMPLETED_TRANSITION header comment in scripts/lib/jira-triage-ops.lib.sh (and rebuilt the bundles) to state (no default; falls back to JIRA_SPLIT_TRANSITION when unset), and updated the PR body to remove the stale "Closed" default / harness/triage.yaml references and note that validation used mocked workflow responses (linking fullsend-ai/fullsend#6704).

Comment thread scripts/lib/jira-triage-ops.lib.sh Outdated
| not
)
] as $done_transitions
| ([$done_transitions[] | select(.name | test("(?i)^(done|closed|resolved|complete(d)?)$"))][0].id // empty)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: Fallback matches only exact transition names, so common workflows still fail; docs describe a broader fallback

The fallback keeps a done-category transition only when its transition .name exactly matches ^(done|closed|resolved|complete(d)?)$. In Jira, the transition name and the target status (.to.name) are separate fields. Transitions named "Resolve Issue", "Close Issue", "Close", "Mark as done", "Finished", "Fixed" and so on do not match, even when they lead to a Done/Closed/Resolved status. The action then still exits 1 with "transition ... is not available" under set -e, which is the failure this PR set out to remove.

docs/triage.md (lines 292-295) promises a fallback to "an available positive Done-category transition (preferring "Done", "Closed", "Resolved", or "Complete")". That wording implies other positive Done transitions are accepted and that there is a priority order. The code accepts only those exact names and takes [0] in API order. Only mocked tests back the change.

Suggestion: Select candidates by .to.statusCategory.key == "done" and match the positive list against (.to.name // .name), keeping the rejection exclusion on both .name and .to.name. Rank matches explicitly if the docs keep the word "preferring". Otherwise narrow docs/triage.md to "the first available transition named exactly Done, Closed, Resolved, or Complete(d)". Please also state in the PR whether this was validated against a live team-managed or company-managed project.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 20bebaa (20bebaa): the fallback selector in scripts/lib/jira-triage-ops.lib.sh now matches positive Done-category names against (.to.name // .name) while keeping rejection exclusions on both .name and .to.name, and ranks tier-1 exact matches (Done, Closed, Resolved, Complete(d)) ahead of tier-2 word-boundary matches so transitions such as "Resolve Issue" -> "Resolved" work cleanly. Added jira-completed-fallback-resolve-issue coverage in scripts/post-triage-test.sh and rebuilt bundles.

is_rejection_status=1
elif [[ -n "${not_planned_name}" && "${not_planned_name,,}" != "${transition_name,,}" && "${current_status_name,,}" == "${not_planned_name,,}" ]]; then
is_rejection_status=1
fi

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: duplicate and not planned treat any rejection status as already handled, and this path has no tests

In the idempotency pre-flight (lines 401-414), any done-category status that matches the broad rejection regex (won't do|not planned|duplicate|obsolete|reject|cancel|invalid|cannot reproduce|abandon), or that equals the dup or not-planned name, returns 0 for both duplicate and not planned. So a not planned action on an issue already in "Duplicate" (or a duplicate action on an issue in "Won't Do" or "Obsolete") reports success and keeps the wrong resolution. Before this PR, the transition was attempted and failed visibly if unavailable.

This is the same class as the earlier "wrong resolution" thread, which was fixed only for completed/split. The new (rejected); skipping transition branch has no test: MOCK_JIRA_STATUS_REJECTED is used only for the split-Obsolete case (scripts/post-triage-test.sh ~2456-2477).

Suggestion: For duplicate and not planned, skip only when the current status equals the configured transition name or its target status; otherwise attempt the transition as before. Add MOCK_JIRA_STATUS_REJECTED tests for duplicate and not planned, including one where the issue is in the other rejection status.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 20bebaa (20bebaa): removed the broad rejection-regex short-circuit for duplicate and not planned so they only skip idempotently when the current Jira status matches the configured transition name or its .to.name target status. Added jira-duplicate-on-wont-do-transitions and jira-not-planned-idempotent-wont-do (plus -no-post) tests in scripts/post-triage-test.sh.

transition_id=$(echo "${transitions}" | jq -r --arg name "${transition_name}" \
'[.transitions[] | select(.name == $name)][0].id // empty')

# Fallback resolution when configured transition name is not directly available

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: An explicitly configured transition that does not exist is now silently replaced by the heuristic fallback

For completed and split, when the configured name is missing from the issue's transitions, the code falls back to any Done/Closed/Resolved/Complete transition. It logs only a plain stdout line and the run succeeds. A typo or stale override (for example JIRA_COMPLETED_TRANSITION="Closd", or a renamed workflow transition) used to fail the post-script. Now it quietly closes the issue through a different transition, with no CI annotation for the operator. No linked issue or ADR records this change to the operator contract; the fullsend Jira user guide points tracker-neutral transition discovery at fullsend-ai/fullsend#6704.

Suggestion: Print the fallback message as a ::warning:: annotation so operators see the substitution in the run summary, and link the PR to fullsend-ai/fullsend#6704 or to an issue that records the new behaviour.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 20bebaa (20bebaa): emitted a ::warning::Transition '...' is not available on issue ...; falling back to '...' via Done-category heuristics annotation (sanitized via _gha_sanitize) when substituting a missing transition, updated jira-completed-fallback-done to assert it, and linked fullsend-ai/fullsend#6704 in the PR description.

Signed-off-by: Jeffery Bagirimvano <jefferyb@redhat.com>
@jefferyb jefferyb changed the title fix(triage): decouple completed from split and resolve Jira transitions dynamically fix(triage): resolve Jira completion transitions dynamically with Done fallback Nov 13, 2026

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review findings: 1 MEDIUM. Details are in the inline comment.

Comment thread scripts/lib/jira-triage-ops.lib.sh Outdated
| select(
($dup != "" and ($dup | ascii_downcase) != ($target | ascii_downcase) and (($n | ascii_downcase) == ($dup | ascii_downcase) or ($to | ascii_downcase) == ($dup | ascii_downcase))) or
($not_planned != "" and ($not_planned | ascii_downcase) != ($target | ascii_downcase) and (($n | ascii_downcase) == ($not_planned | ascii_downcase) or ($to | ascii_downcase) == ($not_planned | ascii_downcase))) or
($n | test("(?i)won.?t do|not.?planned|duplicate|obsolete|reject|cancel|invalid|cannot reproduce|abandon")) or

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: The rejection denylist misses common Jira rejection names, so the completed/split fallback can close an issue as "Won't Fix"

The fallback added in 20bebaa ranks a transition as positive when its name OR its target status (.to.name) matches the tier-1 or tier-2 positive regex (lines 458-459). Rejections are excluded only by a fixed denylist: won.?t do|not.?planned|duplicate|obsolete|reject|cancel|invalid|cannot reproduce|abandon. The same list appears at line 403 (bash grep) and lines 452-453 (jq).

Common Jira rejection names are not on it: "Won't Fix", "Won't Implement", "Declined", "Deferred", "Not a Bug" and "As Designed".

  • Fallback: a transition named "Close Issue" (tier 2 matches \bclose\b on .name), or one named "Closed", whose .to.name is "Won't Fix", passes the filter and can be picked. A completed or split action then closes the issue as rejected, which docs/triage.md says is excluded.
  • Pre-flight (line 403): an issue already in "Won't Fix" or "Declined" gets is_rejection_status=0, so completed skips with "already in status ... (done)" and never moves the issue to a positive Done status.

The earlier Qodo comment covered the older last-resort [0] fallback. This one is about the tier-2 .name/.to.name matching added in 20bebaa.

Suggestion

  1. Do not accept a match on .name when .to.name is present and not itself positive. For tier 1, match only exact positive .to.name values.
  2. Add won.?t (fix|implement)|declin|defer|not a bug|as designed|out of scope to the denylist.
  3. Move the regex into one shared variable and pass it to jq with --arg, so the bash and jq copies cannot drift apart.
  4. Change jira-completed-fallback-done to assert the POST body ({"transition":{"id":"51"}}) instead of only the warning text. Add a mock with a "Close Issue" -> "Won't Fix" transition next to Done.

For a more robust check, read the resolution field (fields=status,resolution) in the pre-flight.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in f7716a7b (f7716a7b5536d3b2c09b37841c1360551c9508e1):

  1. Shared rejection regex (rejection_re) & expanded terms: Extracted the rejection pattern into one shared rejection_re variable in _jira_transition_issue (used by pre-flight grep -qiE and passed to jq via --arg reject_re), adding won.?t (do|fix|implement), declin, defer, not a bug, as designed, and out of scope.
  2. Require positive .to.name when present: Updated tier-1 and tier-2 fallback selection so when .to.name is non-empty, .to.name itself must satisfy the positive pattern (falling back to .name only when .to.name is empty), preventing transitions like Close Issue -> Won't Fix from matching on .name.
  3. Bundled scripts & strengthened tests: Rebuilt all bundled scripts (make -B script-build), verified zero diff with make check-bundle, passed shellcheck -S warning, post-triage-test.sh (both source and bundled), pre-triage-test.sh, and pre-code-jira-test.sh. Updated jira-completed-fallback-done to assert the {"transition":{"id":"51"}} POST payload + warning, and added jira-completed-fallback-skips-close-issue-wont-fix.

…et status

Signed-off-by: Jeffery Bagirimvano <jefferyb@redhat.com>
fi
fi

if [[ -z "${transition_id}" ]]; then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: The Done-category fallback POSTs a transition the operator never configured, and sends no resolution

When the configured transition is missing for completed/split, the fallback (lines 437-471) now picks a heuristic transition. One example is classic "Resolve Issue" -> "Resolved", which the jira-completed-fallback-resolve-issue test models. The code then POSTs only {transition:{id}} here. It does not check hasScreen or expand=transitions.fields, and it does not set fields.resolution.

In company-managed or classic workflows, the transition screen supplies Resolution. Through REST, such a transition either fails validation (400) or leaves the issue Done but "Unresolved". Before this PR, the code only POSTed a transition the operator had chosen. The fallback now does the same with transitions nobody vetted. fullsend-ai/fullsend#6704, which this PR links, lists supplying a resolution as part of the transition as a requirement.

The pre-flight has the same blind spot. It requests fields=status only (line 392), so a "Closed" issue with resolution "Won't Fix" still counts as already completed. An earlier comment mentioned reading the resolution field only as an optional extra. The new part here is the fallback POST.

Suggestion

  1. In the fallback, prefer candidates with hasScreen == false.
  2. Otherwise fetch ?expand=transitions.fields. If the screen has a Resolution field, set fields.resolution (for example "Done" or "Fixed"). If not, skip that candidate and fail loudly.
  3. Add a mock test for a candidate whose screen requires a resolution.
  4. Optional: request fields=status,resolution in the pre-flight and treat a rejection-class resolution like a rejection status.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants