Skip to content

Implement the sprint-overview script#4672

Open
psss wants to merge 3 commits intomainfrom
psss-sprint-overview
Open

Implement the sprint-overview script#4672
psss wants to merge 3 commits intomainfrom
psss-sprint-overview

Conversation

@psss
Copy link
Contributor

@psss psss commented Mar 11, 2026

Added a helper script showing all issues and pull requests included in the given sprint. Provides a human readable text overview and yaml export for further processing. This is the first half of the jira syncing automation. The second part will be completed once the Jira migration is done.

Fix #4604.

Pull Request Checklist

  • implement the feature

@psss psss force-pushed the psss-sprint-overview branch from fcae090 to 32d12ce Compare March 11, 2026 08:04
Added a helper script showing all issues and pull requests included in
the given sprint. Provides a human readable text overview and yaml
export for further processing. This is the first half of the jira
syncing automation. The second part will be completed once the Jira
migration is done.

Fix #4604.

Assisted-by: Claude
@psss psss force-pushed the psss-sprint-overview branch from 32d12ce to 97ac27a Compare March 11, 2026 08:11
gemini-code-assist[bot]

This comment was marked as outdated.

@psss psss added the area | automation Tools, actions and workflows automating our work label Mar 11, 2026
@psss psss added this to planning Mar 11, 2026
@github-project-automation github-project-automation bot moved this to backlog in planning Mar 11, 2026
@psss psss moved this from backlog to review in planning Mar 11, 2026
@psss
Copy link
Contributor Author

psss commented Mar 11, 2026

/gemini review

@psss
Copy link
Contributor Author

psss commented Mar 11, 2026

Example output:

> ./scripts/sprint-overview 'Sprint 11'
Fetching items from teemtee/project/1 for sprint 'Sprint 11'...
  Page 1: 40 items
Total: 40 items


================================================================================
Sprint: Sprint 11
Total items: 40
Total story points: 129
================================================================================

Open Issues (10) - 41 points:
--------------------------------------------------------------------------------
  #1270   [ 3] Clarify `tmt` mission and compare with others
  #3691   [ 1] Move to Public Ranch bare metal instances for plans needing VMs
  #4531   [ 5] Implement full loading of the recipe
  #4557   [ 5] Review flaky tests and make them reliable
  #4604   [ 5] Duplicated issues, Jira Cloud integration, sprint overview
  #4605   [ 8] Move installer logic to package managers
  #4618   [ 5] Save warning messages to be picked up by testing-farm
  #4637   [ 2] Add virtualization.confidential hardware requirement
  #4644   [ 5] Create a Verify Install plugin which checks if packages are installed from the correct repo
  #4645   [ 2] All providers should list the artifacts in artifacts.yaml

Open Pull Requests (8) - 24 points:
--------------------------------------------------------------------------------
  #3694   [ 1] Test virtual & bootc provision plugins in public
  #4280   [ 5] Add Polarion story export with test case linking and custom fields - features inside polarion
  #4638   [ 2] Add `virtualization.confidential` hardware requirement
  #4642   [ 3] Save all warnings encountered in a warnings file
  #4653   [ 3] Consolidate installation logic into package managers
  #4660   [ 5] Add a new feature plugin to verify package,repo combination
  #4661   [ 3] Add support for full recipe loading
  #4672   [ 2] Implement the `sprint-overview` script

Closed Issues (7) - 28 points:
--------------------------------------------------------------------------------
  #1793   [ 5] Support `environment` in the `test` key of `fmf` discover
  #3894   [ 5] Support multiple `discover` phases in `upgrade` execute
  #4440   [ 5] Library conflicts with different url
  #4519   [ 5] `tmt-reboot` does not work with `container` plugin
  #4578   [ 3] Improve test coverage for results handling and interpretation
  #4602   [ 3] Outline the consistent approach for handling paths
  #4608   [ 2] Kickstart template for `libvirt` cannot be found

Merged Pull Requests (15) - 36 points:
--------------------------------------------------------------------------------
  #4326   [ 3] Move plan stuff in its own submodule
  #4416   [ 3] Enforce test duration across reboots and restarts
  #4503   [ 3] Allow beakerlib libraries with clashing repo and name
  #4549   [ 5] Add integration test for NVR edge cases 
  #4564   [ 2] Detect and error on duplicate artifact NVRAs
  #4589   [ 3] Add a unit test for check result combinations
  #4615   [ 2] Rework Github release workflow
  #4619   [ 2] Deal with Fedora ELN no longer shipping legacy dnf tooling
  #4621   [ 3] Allow adjusting individual tests
  #4622   [ 1] Update Pre-commit Dependencies
  #4630   [ 1] Skip known bootc failure for now
  #4633   [ 3] Fix support for multiple discover phases in upgrade execute plugin
  #4643   [ 2] Force use of tty in `podman exec` for test session
  #4648   [ 1] Do not check for journal.xml presence, temporarily
  #4656   [ 2] Try to get a more reasonable stacklevel

================================================================================
Summary: 18 open (65 pts), 22 closed/merged (64 pts)
================================================================================

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new script, sprint-overview, to provide an overview of issues and pull requests for a given sprint. The script is well-structured and provides both human-readable and YAML output formats. I've identified a potential security/correctness issue related to handling sprint names with special characters, and a couple of areas where the code can be refactored for better readability and maintainability.

"orgName": org,
"projectNumber": project,
"cursor": cursor,
"query": f'Sprint:"{sprint_name}"',
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The sprint_name is used directly in the GraphQL query string. If the sprint name contains special characters, such as a double quote, it could break the query or lead to incorrect results. Sanitize the sprint name by escaping double quotes before incorporating it into the query string.

Suggested change
"query": f'Sprint:"{sprint_name}"',
"query": f'Sprint:"{sprint_name.replace('"', '\\"')}"',

Comment on lines +175 to +181
key = (
"open_prs"
if state == "OPEN"
else "merged_prs"
if state == "MERGED"
else "closed_prs"
)
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This nested ternary operator is difficult to read. Replace it with a standard if/elif/else block to improve code clarity and maintainability.

            if state == "OPEN":
                key = "open_prs"
            elif state == "MERGED":
                key = "merged_prs"
            else:
                key = "closed_prs"

Comment on lines +256 to +328
output = {
"sprint": sprint_name,
"total": {
"items": total,
"story_points": int(sum(total_points.values())),
},
"categories": {
"open_issues": {
"count": len(categories["open_issues"]),
"story_points": total_points["open_issues"],
"items": [
{
"url": url,
"title": title,
"story_points": int(size) if size is not None else None,
}
for _, title, size, url in categories["open_issues"]
],
},
"open_prs": {
"count": len(categories["open_prs"]),
"story_points": total_points["open_prs"],
"items": [
{
"url": url,
"title": title,
"story_points": int(size) if size is not None else None,
}
for _, title, size, url in categories["open_prs"]
],
},
"closed_issues": {
"count": len(categories["closed_issues"]),
"story_points": total_points["closed_issues"],
"items": [
{
"url": url,
"title": title,
"story_points": int(size) if size is not None else None,
}
for _, title, size, url in categories["closed_issues"]
],
},
"merged_prs": {
"count": len(categories["merged_prs"]),
"story_points": total_points["merged_prs"],
"items": [
{
"url": url,
"title": title,
"story_points": int(size) if size is not None else None,
}
for _, title, size, url in categories["merged_prs"]
],
},
"closed_prs": {
"count": len(categories["closed_prs"]),
"story_points": total_points["closed_prs"],
"items": [
{
"url": url,
"title": title,
"story_points": int(size) if size is not None else None,
}
for _, title, size, url in categories["closed_prs"]
],
},
},
"summary": {
"open": {"count": open_count, "story_points": int(open_points)},
"closed": {"count": closed_count, "story_points": int(closed_points)},
},
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The logic for building each category in the YAML output is duplicated. Refactor this to follow the Don't Repeat Yourself (DRY) principle, which will make the code more maintainable. You can use a dictionary comprehension to generate the categories dictionary.

For example:

    category_keys = (
        "open_issues",
        "open_prs",
        "closed_issues",
        "merged_prs",
        "closed_prs",
    )
    output_categories = {
        key: {
            "count": len(categories[key]),
            "story_points": total_points[key],
            "items": [
                {
                    "url": url,
                    "title": title,
                    "story_points": int(size) if size is not None else None,
                }
                for _, title, size, url in categories[key]
            ],
        }
        for key in category_keys
    }

    output = {
        "sprint": sprint_name,
        "total": {
            "items": total,
            "story_points": int(sum(total_points.values())),
        },
        "categories": output_categories,
        "summary": {
            "open": {"count": open_count, "story_points": int(open_points)},
            "closed": {"count": closed_count, "story_points": int(closed_points)},
        },
    }

Comment on lines +376 to +379
token = os.environ.get("GITHUB_PERSONAL_ACCESS_TOKEN")
if not token:
print("Error: GITHUB_PERSONAL_ACCESS_TOKEN environment variable not set", file=sys.stderr)
sys.exit(1)
Copy link
Contributor

Choose a reason for hiding this comment

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

Unless we are doing write requests, we do not need a GITHUB_PERSONAL_ACCESS_TOKEN. From what I see, this is doing only reads on PRs, so I think it could be refactored to use the rest api as read-only instead

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, without the token, we're not able to access the project data. I had to explicitly allow the read:project permission for that token.

Copy link
Contributor

Choose a reason for hiding this comment

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

Really? I can see them just fine from an incognito window. Does read:project restrict all reads or only reads to private repo/projects (the same like read:repository is unnecessary actually because of this).

If we do indeed need it, there should be a better way around this with github apps. Personal access tokens are a pain to manage, and would really like to avoid that hard dependency

@@ -0,0 +1,403 @@
#!/usr/bin/python3
Copy link
Contributor

@LecrisUT LecrisUT Mar 11, 2026

Choose a reason for hiding this comment

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

Could you include the PEP723 header like

Suggested change
#!/usr/bin/python3
#!/usr/bin/python3
# /// script
# dependencies = [
# "click",
# "requests",
# "ruamel.yaml",
# ]
# ///

Comment on lines +5 to +8
Usage:
sprint-overview "Sprint 11"
sprint-overview "Sprint 11" --org myorg --project 2
sprint-overview "Sprint 11" --yaml
Copy link
Contributor

Choose a reason for hiding this comment

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

Usage is already defined in the help message, let's not introduce a source of divergence

Comment on lines +363 to +374
parser.add_argument("sprint_name", help="Name of the sprint (e.g., 'Sprint 11')")
parser.add_argument(
"--org", default="teemtee", help="GitHub organization name (default: teemtee)"
)
parser.add_argument("--project", type=int, default=1, help="Project number (default: 1)")
parser.add_argument(
"--yaml",
action="store_true",
help="Output in YAML format for machine-readable processing",
)

args = parser.parse_args()
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we not just hard-code these? Maybe at most having either yaml format or the format to be used in the slack messages.

Comment on lines +217 to +219
print(f"Sprint: {sprint_name}")
print(f"Total items: {total}")
print(f"Total story points: {sum(total_points.values()):.0f}")
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't like the format being defined by all of these print commands. Can we have at least an f-string of the overall format?

Copy link
Contributor

Choose a reason for hiding this comment

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

Or a template. Like Jinja. Exactly the task for which templating engines were invented...

I don't understand why people insist on building complex, multiline, structured text output with raw print() calls... It's like there's some bonus for suffering. Yeah, let me compete in the table tennis tournament, but I insist on having both arms tied behind my back, holding paddle in my mouth!

Copy link
Contributor

Choose a reason for hiding this comment

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

And the whole concept of separating logic from the presentation layer. Interweaving both together makes maintenance harder.

Comment on lines +61 to +97
query = """
query($orgName: String!, $projectNumber: Int!, $cursor: String, $query: String!) {
organization(login: $orgName) {
projectV2(number: $projectNumber) {
items(first: 100, after: $cursor, query: $query) {
nodes {
content {
... on Issue {
__typename
number
title
state
url
}
... on PullRequest {
__typename
number
title
state
url
}
}
size: fieldValueByName(name: "Size") {
... on ProjectV2ItemFieldNumberValue {
number
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
"""
Copy link
Contributor

Choose a reason for hiding this comment

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

Uph, isn't this super dark-magic for us? At least with the REST API we can point to the documentation of what is being requested: https://docs.github.com/en/rest/projects/items?apiVersion=2022-11-28#get-an-item-for-an-organization-owned-project

Copy link
Contributor

@thrix thrix left a comment

Choose a reason for hiding this comment

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

Code Review: Implement the sprint-overview script

Reviewed against commit 7d37dae.

Summary

A helper script that fetches sprint items from GitHub Projects via GraphQL and displays them in human-readable text or YAML format. Well-structured with pagination support. There's extensive existing review feedback from @LecrisUT, @happz, Gemini, and Graphite — I'll focus on items not yet covered.

Issues

1. YAML output omits the issue/PR number (minor)

Each YAML item has url, title, story_points but not number. The text output prominently shows #4672, but the YAML output (intended for machine-readable processing) requires URL parsing to get the number. Since number is already available in the tuple, it should be included. See inline comment.

2. Silent skip of DraftIssues could be confusing (minor)

if item.get("content"): silently skips draft issues (which have no content type in GitHub Projects). A comment explaining this would help. See inline comment.

Agreement with existing feedback

The following open review comments from other reviewers are valid and worth addressing:

  • @LecrisUT: PEP723 header — Adding dependency metadata (click, requests, pyyaml) enables uv run scripts/sprint-overview without manual dependency management.
  • @LecrisUT/@happz: Output formatting — The print()-based formatting is verbose and tightly couples logic with presentation. A template (Jinja or even a simple f-string template) would be cleaner and easier to maintain.
  • @gemini: DRY for YAML output — The 5x repeated category block in display_items_yaml should use a loop/comprehension.
  • @LecrisUT: GraphQL vs REST API — Worth investigating if the REST API can access project items without a token, reducing the setup barrier.

Verdict

Functional and useful script. The existing review feedback covers the main improvement areas well. The missing number in YAML output (issue #1) is the main new finding.

Generated-by: Claude Code

"title": title,
"story_points": int(size) if size is not None else None,
}
for _, title, size, url in categories["open_issues"]
Copy link
Contributor

Choose a reason for hiding this comment

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

The number (issue/PR number) is discarded here with _, so YAML items only have url, title, story_points. The text output prominently shows #number, but the YAML output (intended for machine-readable processing per the --yaml help text) requires URL parsing to extract it. Since number is already in the tuple, consider including it:

{
    "number": number,
    "url": url,
    "title": title,
    "story_points": int(size) if size is not None else None,
}
for number, title, size, url in categories["open_issues"]

Same applies to the other 4 category blocks below.

Generated-by: Claude Code

print(f" Page {page}: {len(items)} items", file=sys.stderr)

for item in items:
if item.get("content"):
Copy link
Contributor

Choose a reason for hiding this comment

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

This silently skips draft issues (which have no content in GitHub Projects). A brief comment explaining the intent would help — e.g. # Skip draft issues which have no content type.

Generated-by: Claude Code

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

Labels

area | automation Tools, actions and workflows automating our work

Projects

Status: review

Development

Successfully merging this pull request may close these issues.

Duplicated issues, Jira Cloud integration, sprint overview

5 participants