-
Notifications
You must be signed in to change notification settings - Fork 122
Add Jira sprint support for 'did this sprint' / 'did last sprint' #475
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
evakhoni
wants to merge
6
commits into
psss:main
Choose a base branch
from
evakhoni:feature/jira-sprint-support
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
9420b17
Add Jira sprint support for did this/last sprint
evakhoni fb55d24
Fix pre-commit linter issues in sprint support
evakhoni 254fd3c
Address reviewer feedback on sprint support
evakhoni 4cfd816
Unify sprint selection logic for active and closed sprints
evakhoni 0ffb7dd
Move jira_group instantiation before board discovery logic
evakhoni 227c920
Error on multi-project config without sprint_board
evakhoni File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -697,6 +697,159 @@ | |
| log.debug("Num worklogs after filtering: %d", len(issue.worklogs)) | ||
| self.stats = [issue for issue in issues if len(issue.worklogs) > 0] | ||
|
|
||
| # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
| # Sprint Support | ||
| # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
|
|
||
| def get_sprint_dates(last: bool = False) -> tuple: | ||
| """ | ||
| Fetch sprint dates from Jira Agile API. | ||
|
|
||
| Returns (since, until, sprint_name) tuple with the sprint's | ||
| date range. If ``last`` is True, returns the most recently | ||
| closed sprint; otherwise returns the active sprint. | ||
|
|
||
| Auto-discovers the Scrum board from the project config, or | ||
| uses the sprint_board config if provided. | ||
| """ | ||
| # Import Date here to avoid circular dependency | ||
Check noticeCode scanning / CodeQL Cyclic import Note
Import of module
did.base Error loading related location Loading |
||
| # (base.py -> jira.py -> base.py) | ||
| # pylint: disable=import-outside-toplevel,too-many-locals | ||
| # pylint: disable=too-many-branches,too-many-statements | ||
| from did.base import Date | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same as in #475 (comment) just from the other end |
||
|
|
||
| # Read Jira config | ||
| try: | ||
| config = dict(Config().section("jira")) | ||
| except Exception as error: | ||
| raise ReportError( | ||
| "No [jira] section found in config. Sprint support requires " | ||
| "a configured Jira integration.") from error | ||
|
|
||
| jira_group = JiraStatsGroup(option="jira") | ||
|
|
||
| # Get board ID - either from config or auto-discover | ||
| board_id: Optional[int] = None | ||
| if "sprint_board" in config: | ||
| try: | ||
| board_id = int(config["sprint_board"]) | ||
| log.debug("Using sprint_board from config: %s", board_id) | ||
| except ValueError as error: | ||
| raise ReportError( | ||
| f"Invalid sprint_board value '{config['sprint_board']}'. " | ||
| "Must be a numeric board ID.") from error | ||
| else: | ||
| # Auto-discover from project | ||
| if "project" not in config: | ||
| raise ReportError( | ||
| "Neither 'sprint_board' nor 'project' is set in [jira] config. " | ||
| "Sprint support requires one of them.") | ||
|
|
||
| project_key = config["project"].strip() | ||
| if "," in project_key: | ||
| raise ReportError( | ||
| "Multiple projects configured in [jira] config. " | ||
| "Set sprint_board to specify which board to use " | ||
| "for sprint dates.") | ||
| log.debug("Auto-discovering Scrum board for project %s", project_key) | ||
|
|
||
| # Query Agile API for boards | ||
| boards_url = ( | ||
| f"{jira_group.url}/rest/agile/1.0/board" | ||
| f"?projectKeyOrId={project_key}&type=scrum" | ||
| ) | ||
|
|
||
| try: | ||
| response = jira_group.session.get( | ||
| boards_url, | ||
| timeout=jira_group.timeout) | ||
| response.raise_for_status() | ||
| boards_data = response.json() | ||
| except requests.exceptions.RequestException as error: | ||
| raise ReportError( | ||
| f"Failed to fetch Scrum boards for project {project_key}: {error}" | ||
| ) from error | ||
|
|
||
| boards = boards_data.get("values", []) | ||
| if not boards: | ||
| raise ReportError( | ||
| f"No Scrum boards found for project {project_key}. " | ||
| "Make sure the project has a Scrum board configured, or " | ||
| "set sprint_board manually in [jira] config.") | ||
|
|
||
| if len(boards) > 1: | ||
| board_list = ", ".join( | ||
| f"{b['id']} ({b['name']})" for b in boards) | ||
| raise ReportError( | ||
| f"Multiple Scrum boards found for project {project_key}: " | ||
| f"{board_list}. Set sprint_board in [jira] config to choose one.") | ||
|
|
||
| board_id = boards[0]["id"] | ||
| log.debug("Auto-discovered board ID: %s (%s)", | ||
| board_id, boards[0]["name"]) | ||
|
|
||
| if board_id is None: | ||
| # This should never happen due to validation above | ||
| raise ReportError("Failed to determine board ID") | ||
|
|
||
| # Query for sprints | ||
| state = "closed" if last else "active" | ||
| sprints_url = ( | ||
| f"{jira_group.url}/rest/agile/1.0/board/{board_id}/sprint" | ||
| f"?state={state}" | ||
| ) | ||
|
|
||
| try: | ||
| response = jira_group.session.get( | ||
| sprints_url, | ||
| timeout=jira_group.timeout) | ||
| response.raise_for_status() | ||
| sprints_data = response.json() | ||
| except requests.exceptions.RequestException as error: | ||
| raise ReportError( | ||
| f"Failed to fetch {state} sprints for board {board_id}: {error}" | ||
| ) from error | ||
|
|
||
| sprints = sprints_data.get("values", []) | ||
| if not sprints: | ||
| raise ReportError( | ||
| f"No {state} sprints found for board {board_id}.") | ||
|
|
||
| # Sort by endDate descending and take the most recent one. | ||
| # This handles both "last sprint" (most recently closed) and | ||
| # "this sprint" (if parallel sprints are enabled, picks the | ||
| # one ending soonest). | ||
| sprints_with_dates = [ | ||
| s for s in sprints | ||
| if s.get("endDate") | ||
| ] | ||
| if not sprints_with_dates: | ||
| raise ReportError( | ||
| f"No {state} sprints with end dates found " | ||
| f"for board {board_id}.") | ||
| sprints_with_dates.sort( | ||
| key=lambda s: dateutil.parser.parse(s["endDate"]), | ||
| reverse=True) | ||
| sprint = sprints_with_dates[0] | ||
|
|
||
| # Parse dates | ||
| if "startDate" not in sprint or "endDate" not in sprint: | ||
| raise ReportError( | ||
| f"Sprint {sprint.get('name', sprint.get('id'))} is missing " | ||
| "startDate or endDate.") | ||
|
|
||
| start_date = dateutil.parser.parse(sprint["startDate"]).date() | ||
| end_date = dateutil.parser.parse(sprint["endDate"]).date() | ||
| sprint_name = sprint.get("name", f"Sprint {sprint['id']}") | ||
|
|
||
| return ( | ||
| Date(str(start_date)), | ||
| Date(str(end_date)), | ||
| sprint_name | ||
| ) | ||
|
|
||
|
|
||
| # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
| # Stats Group | ||
| # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This cycle is intentional — base.py needs to dispatch did this sprint to the Jira plugin, and the Jira plugin needs Date from base.py. The import is lazy (inside Date.period(), only executed when the user actually requests a sprint period), so the cycle is broken at runtime. This is the standard Python pattern for unavoidable circular dependencies.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@psss I'm not able to verify if this is a pythonic way to handle this. Can you?