Skip to content

Commit 95b72e0

Browse files
feat(rhdh-release): use JQL Team[Team] filter with Cloud ID for team queries
Replace the slow parse_issues.py --enrich workflow for team-based filtering with direct JQL filtering using "Team[Team]" = "{{CLOUD_ID}}". This reduces team breakdown queries from minutes (per-issue API enrichment) to ~12 seconds (one JQL count per team). Changes: - Add 3 new JQL templates: open_issues_by_team, feature_freeze_issues_by_team, code_freeze_issues_by_team - Add cloud_id placeholder support to jql.py render/render_with_url - Rewrite cmd_team_breakdown, cmd_slack_feature_freeze_update, and cmd_slack_code_freeze_update in release.py to use JQL team filter - Update workflow docs and SKILL.md to reference Cloud ID approach Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1540a7d commit 95b72e0

6 files changed

Lines changed: 78 additions & 55 deletions

File tree

skills/rhdh-release/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ compatibility: "acli on PATH. Python 3 + gog CLI for Google Sheets/Docs."
1111

1212
<essential_principles>
1313

14-
<principle name="use_parse_issues_for_team_counts">
15-
Always use `parse_issues.py --enrich` for team counts — never count manually. The Team custom field cannot be queried via JQL. Use `acli jira workitem search --json | python ~/.claude/skills/rhdh-jira/scripts/parse_issues.py --enrich` and filter by team in the output.
14+
<principle name="use_cloud_id_for_team_queries">
15+
Use `"Team[Team]" = "{{CLOUD_ID}}"` in JQL to filter by team. Cloud IDs are in the RHDH Team Mapping spreadsheet (column "Cloud ID"). Use the `open_issues_by_team`, `feature_freeze_issues_by_team`, or `code_freeze_issues_by_team` JQL templates. This is fast and does not require `parse_issues.py --enrich`.
1616
</principle>
1717

1818
<principle name="include_jira_links">

skills/rhdh-release/references/jql-release.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,37 @@ project IN (RHIDP, RHDHBugs, RHDHPLAN, RHDHSUPP) AND fixVersion = "{{RELEASE_VER
156156

157157
- **Placeholders:** `{{RELEASE_VERSION}}`
158158
- **Notes:** All open work. Same as `open_issues` — used for Code Freeze announcements.
159+
160+
## open_issues_by_team
161+
162+
Find all open issues for a release filtered by team using Cloud ID.
163+
164+
```jql
165+
project IN (RHIDP, RHDHBugs, RHDHPLAN, RHDHSUPP) AND fixVersion = "{{RELEASE_VERSION}}" AND status != closed AND "Team[Team]" = "{{CLOUD_ID}}"
166+
```
167+
168+
- **Placeholders:** `{{RELEASE_VERSION}}`, `{{CLOUD_ID}}`
169+
- **Example:** `... AND fixVersion = "2.1.0" AND status != closed AND "Team[Team]" = "ec74d716-af36-4b3c-950f-f79213d08f71-4403"`
170+
- **Notes:** Cloud ID is the Jira Cloud team identifier from the RHDH Team Mapping spreadsheet (column "Cloud ID"). This is the fastest way to filter by team — no enrichment needed.
171+
172+
## feature_freeze_issues_by_team
173+
174+
Find feature work outstanding at Feature Freeze filtered by team.
175+
176+
```jql
177+
project IN (RHIDP, RHDHBugs, RHDHPLAN, RHDHSUPP) AND fixVersion = "{{RELEASE_VERSION}}" and resolution is EMPTY AND component not in (AI, Build, Certification, "Continuous Improvement", Documentation, Knowledge, Performance, Quality, Quickstart, Release, "RHDH Local", Security, Segment, Serviceability, Support, "Team Operations", "Test Framework", "Test Infrastructure", "Upstream & Community", UX) AND Type not in (Bug, Vulnerability, sub-task) AND status not in ("Dev Complete", "Release Pending", Done, Closed) AND (labels is EMPTY OR labels != stretch-goal) AND "Team[Team]" = "{{CLOUD_ID}}"
178+
```
179+
180+
- **Placeholders:** `{{RELEASE_VERSION}}`, `{{CLOUD_ID}}`
181+
- **Notes:** Same as `feature_freeze_issues` but scoped to a single team by Cloud ID.
182+
183+
## code_freeze_issues_by_team
184+
185+
Find all issues outstanding at Code Freeze filtered by team.
186+
187+
```jql
188+
project IN (RHIDP, RHDHBugs, RHDHPLAN, RHDHSUPP) AND fixVersion = "{{RELEASE_VERSION}}" AND status != closed AND "Team[Team]" = "{{CLOUD_ID}}"
189+
```
190+
191+
- **Placeholders:** `{{RELEASE_VERSION}}`, `{{CLOUD_ID}}`
192+
- **Notes:** Same as `code_freeze_issues` but scoped to a single team by Cloud ID.

skills/rhdh-release/scripts/jql.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def render(
7373
*,
7474
version: str | None = None,
7575
issue_type: str | None = None,
76+
cloud_id: str | None = None,
7677
path: Path | None = None,
7778
) -> str:
7879
"""Render a JQL template with placeholder substitution."""
@@ -81,6 +82,8 @@ def render(
8182
jql = jql.replace("{{RELEASE_VERSION}}", version)
8283
if issue_type is not None:
8384
jql = jql.replace("{{ISSUE_TYPE}}", issue_type)
85+
if cloud_id is not None:
86+
jql = jql.replace("{{CLOUD_ID}}", cloud_id)
8487
return jql
8588

8689

@@ -94,10 +97,11 @@ def render_with_url(
9497
*,
9598
version: str | None = None,
9699
issue_type: str | None = None,
100+
cloud_id: str | None = None,
97101
path: Path | None = None,
98102
) -> tuple[str, str]:
99103
"""Render a JQL template and return (jql, jira_url)."""
100-
jql = render(name, version=version, issue_type=issue_type, path=path)
104+
jql = render(name, version=version, issue_type=issue_type, cloud_id=cloud_id, path=path)
101105
return jql, jira_url(jql)
102106

103107

skills/rhdh-release/scripts/release.py

Lines changed: 27 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -619,39 +619,32 @@ def cmd_teams(args: argparse.Namespace, fmt: OutputFormatter) -> None:
619619

620620

621621
def cmd_team_breakdown(args: argparse.Namespace, fmt: OutputFormatter) -> None:
622-
"""Per-team issue counts for a release (requires parse_issues.py --enrich)."""
622+
"""Per-team issue counts for a release using JQL team filter."""
623623
version = args.version
624-
jql = jql_mod.render("open_issues", version=version)
625-
issues = _acli_json_enriched(jql, select="key,summary,status,team")
626-
627624
teams = _fetch_teams(category="Engineering")
628625

629-
counts: dict[str, int] = {}
630-
for issue in issues:
631-
team = issue.get("team", "Unassigned") or "Unassigned"
632-
norm = _normalize_team_name(team)
633-
counts[norm] = counts.get(norm, 0) + 1
634-
635626
rows = []
636627
for t in teams:
637628
name = t["team_name"]
638-
norm = _normalize_team_name(name)
639-
count = counts.pop(norm, 0)
629+
cid = t.get("cloud_id", "")
630+
if not cid:
631+
rows.append({"team_name": name, "cloud_id": "", "count": 0,
632+
"leads": t.get("leads", ""), "slack_handles": t.get("slack_handles", [])})
633+
continue
634+
jql, url = jql_mod.render_with_url("open_issues_by_team", version=version, cloud_id=cid)
635+
count = _acli_count(jql, fmt)
636+
fmt.log_info(f"{name:<25} {count:>5}")
640637
rows.append(
641638
{
642639
"team_name": name,
643-
"team_id": t.get("team_id"),
640+
"cloud_id": cid,
644641
"count": count,
642+
"jira_url": url,
645643
"leads": t.get("leads", ""),
646644
"slack_handles": t.get("slack_handles", []),
647645
}
648646
)
649647

650-
matched_norms = {_normalize_team_name(t["team_name"]) for t in teams}
651-
for norm_name, count in sorted(counts.items()):
652-
if norm_name not in matched_norms:
653-
rows.append({"team_name": norm_name, "count": count})
654-
655648
fmt.header(f"RHDH {version} — Issues by Team")
656649
for r in rows:
657650
fmt.log_info(f"{r['team_name']:<25} {r['count']:>5}")
@@ -793,27 +786,23 @@ def cmd_slack_feature_freeze_update(args: argparse.Namespace, fmt: OutputFormatt
793786
rn_jql, rn_url = jql_mod.render_with_url("release_notes", version=version)
794787
rn_count = _acli_count(rn_jql, fmt)
795788

796-
ff_jql = jql_mod.render("feature_freeze_issues", version=version)
797-
issues = _acli_json_enriched(ff_jql, select="key,summary,status,team")
798-
799-
team_counts: dict[str, int] = {}
800-
for issue in issues:
801-
team = issue.get("team", "Unassigned") or "Unassigned"
802-
norm = _normalize_team_name(team)
803-
team_counts[norm] = team_counts.get(norm, 0) + 1
804-
805789
team_lines = []
806790
for t in teams:
807791
name = t["team_name"]
808-
norm = _normalize_team_name(name)
809-
count = team_counts.get(norm, 0)
792+
cid = t.get("cloud_id", "")
793+
if not cid:
794+
continue
795+
jql, url = jql_mod.render_with_url(
796+
"feature_freeze_issues_by_team", version=version, cloud_id=cid
797+
)
798+
count = _acli_count(jql, fmt)
810799
slack_handles = t.get("slack_handles", [])
811800
lead_slack = slack_handles[0] if slack_handles else t.get("leads", "")
812801
team_lines.append(
813802
{
814803
"TEAM_NAME": name,
815804
"ISSUE_COUNT": str(count),
816-
"JIRA_LINK": jql_mod.jira_url(ff_jql),
805+
"JIRA_LINK": url,
817806
"LEAD_SLACK": lead_slack,
818807
}
819808
)
@@ -898,27 +887,23 @@ def cmd_slack_code_freeze_update(args: argparse.Namespace, fmt: OutputFormatter)
898887
fs_jql, fs_url = jql_mod.render_with_url("feature_subtasks", version=version)
899888
fs_count = _acli_count(fs_jql, fmt)
900889

901-
cf_jql = jql_mod.render("code_freeze_issues", version=version)
902-
issues = _acli_json_enriched(cf_jql, select="key,summary,status,team")
903-
904-
team_counts: dict[str, int] = {}
905-
for issue in issues:
906-
team = issue.get("team", "Unassigned") or "Unassigned"
907-
norm = _normalize_team_name(team)
908-
team_counts[norm] = team_counts.get(norm, 0) + 1
909-
910890
team_lines = []
911891
for t in teams:
912892
name = t["team_name"]
913-
norm = _normalize_team_name(name)
914-
count = team_counts.get(norm, 0)
893+
cid = t.get("cloud_id", "")
894+
if not cid:
895+
continue
896+
jql, url = jql_mod.render_with_url(
897+
"code_freeze_issues_by_team", version=version, cloud_id=cid
898+
)
899+
count = _acli_count(jql, fmt)
915900
slack_handles = t.get("slack_handles", [])
916901
lead_slack = slack_handles[0] if slack_handles else t.get("leads", "")
917902
team_lines.append(
918903
{
919904
"TEAM_NAME": name,
920905
"TEAM_ISSUE_COUNT": str(count),
921-
"JIRA_LINK": jql_mod.jira_url(cf_jql),
906+
"JIRA_LINK": url,
922907
"LEAD_SLACK": lead_slack,
923908
}
924909
)

skills/rhdh-release/workflows/issues-by-team.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,32 +27,32 @@ If the CLI succeeds, use its output directly. If it fails, follow the manual ste
2727
gog sheets get 1vQXfvID72qwqvLb17eyGOvnZXrZG7NBzTGv6RP9wvyM Team --json --results-only
2828
```
2929

30-
Filter to category "Engineering" and status "Active". This gives team names and `team_id` values.
30+
Filter to category "Engineering" and status "Active". This gives team names and `cloud_id` values.
3131

32-
## Step 3 (fallback): Query issues and filter by team
32+
## Step 3 (fallback): Query issues by team using Cloud ID
3333

34-
Fetch all open issues for the release, enriched with team data:
34+
Use the `open_issues_by_team` JQL template with the team's Cloud ID:
3535

3636
```bash
37-
acli jira workitem search --jql 'project IN (RHIDP, RHDHBugs, RHDHPLAN, RHDHSUPP) AND fixVersion = "{{RELEASE_VERSION}}" AND status != closed' --limit 200 --json | python ~/.claude/skills/rhdh-jira/scripts/parse_issues.py --enrich -s key,summary,status,team
37+
acli jira workitem search --jql 'project IN (RHIDP, RHDHBugs, RHDHPLAN, RHDHSUPP) AND fixVersion = "{{RELEASE_VERSION}}" AND status != closed AND "Team[Team]" = "{{CLOUD_ID}}"' --count
3838
```
3939

40-
To filter to a specific team by team ID:
40+
To get full issue details for a team:
4141

4242
```bash
43-
acli jira workitem search --jql 'project IN (RHIDP, RHDHBugs, RHDHPLAN, RHDHSUPP) AND fixVersion = "{{RELEASE_VERSION}}" AND status != closed' --limit 200 --json | python ~/.claude/skills/rhdh-jira/scripts/parse_issues.py --enrich -f team_id={{TEAM_ID}} -s key,summary,status
43+
acli jira workitem search --jql 'project IN (RHIDP, RHDHBugs, RHDHPLAN, RHDHSUPP) AND fixVersion = "{{RELEASE_VERSION}}" AND status != closed AND "Team[Team]" = "{{CLOUD_ID}}"' --json
4444
```
4545

46-
**Important:** Always use `parse_issues.py --enrich` for team counts — the Team field is a custom field that cannot be queried via JQL directly.
46+
The Cloud ID for each team is in the "Cloud ID" column of the RHDH Team Mapping spreadsheet (e.g., `ec74d716-af36-4b3c-950f-f79213d08f71-4403` for COPE).
4747

4848
## Step 4 (fallback): Build per-team counts
4949

5050
For each active engineering team, count the matching issues and build a Jira search link.
5151

5252
## Step 5 (fallback): Format output
5353

54-
| Team | Team ID | Issue Count | Lead | Jira Link |
55-
|------|---------|-------------|------|-----------|
54+
| Team | Cloud ID | Issue Count | Lead | Jira Link |
55+
|------|----------|-------------|------|-----------|
5656
| {{TEAM_NAME}} | {{TEAM_ID}} | {{COUNT}} | @{{LEAD_SLACK}} | [View](https://issues.redhat.com/issues/?jql=...) |
5757

5858
</process>

skills/rhdh-release/workflows/teams-and-leads.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Include link to source: [RHDH Team Mapping](https://docs.google.com/spreadsheets
5151
<gotchas>
5252

5353
- By default only active teams are returned. The CLI filters by `status = Active` in the Google Sheet.
54-
- The `team_id` is the numeric ID used by `parse_issues.py --enrich` for team-based filtering — the Team custom field cannot be queried via JQL directly.
54+
- The `cloud_id` is the Jira Cloud team identifier used by `parse_issues.py --enrich` for team-based filtering — the Team custom field cannot be queried via JQL directly.
5555

5656
</gotchas>
5757

0 commit comments

Comments
 (0)