From 95b72e069eaf3b49607f8a817adf271713c6a148 Mon Sep 17 00:00:00 2001 From: Jason Chui Date: Fri, 3 Jul 2026 10:20:18 -0400 Subject: [PATCH] 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 --- skills/rhdh-release/SKILL.md | 4 +- skills/rhdh-release/references/jql-release.md | 34 +++++++++ skills/rhdh-release/scripts/jql.py | 6 +- skills/rhdh-release/scripts/release.py | 69 ++++++++----------- .../rhdh-release/workflows/issues-by-team.md | 18 ++--- .../rhdh-release/workflows/teams-and-leads.md | 2 +- 6 files changed, 78 insertions(+), 55 deletions(-) diff --git a/skills/rhdh-release/SKILL.md b/skills/rhdh-release/SKILL.md index 2d23d93..7eacba3 100644 --- a/skills/rhdh-release/SKILL.md +++ b/skills/rhdh-release/SKILL.md @@ -11,8 +11,8 @@ compatibility: "acli on PATH. Python 3 + gog CLI for Google Sheets/Docs." - -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. + +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`. diff --git a/skills/rhdh-release/references/jql-release.md b/skills/rhdh-release/references/jql-release.md index afc92c3..f202c2f 100644 --- a/skills/rhdh-release/references/jql-release.md +++ b/skills/rhdh-release/references/jql-release.md @@ -156,3 +156,37 @@ project IN (RHIDP, RHDHBugs, RHDHPLAN, RHDHSUPP) AND fixVersion = "{{RELEASE_VER - **Placeholders:** `{{RELEASE_VERSION}}` - **Notes:** All open work. Same as `open_issues` — used for Code Freeze announcements. + +## open_issues_by_team + +Find all open issues for a release filtered by team using Cloud ID. + +```jql +project IN (RHIDP, RHDHBugs, RHDHPLAN, RHDHSUPP) AND fixVersion = "{{RELEASE_VERSION}}" AND status != closed AND "Team[Team]" = "{{CLOUD_ID}}" +``` + +- **Placeholders:** `{{RELEASE_VERSION}}`, `{{CLOUD_ID}}` +- **Example:** `... AND fixVersion = "2.1.0" AND status != closed AND "Team[Team]" = "ec74d716-af36-4b3c-950f-f79213d08f71-4403"` +- **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. + +## feature_freeze_issues_by_team + +Find feature work outstanding at Feature Freeze filtered by team. + +```jql +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}}" +``` + +- **Placeholders:** `{{RELEASE_VERSION}}`, `{{CLOUD_ID}}` +- **Notes:** Same as `feature_freeze_issues` but scoped to a single team by Cloud ID. + +## code_freeze_issues_by_team + +Find all issues outstanding at Code Freeze filtered by team. + +```jql +project IN (RHIDP, RHDHBugs, RHDHPLAN, RHDHSUPP) AND fixVersion = "{{RELEASE_VERSION}}" AND status != closed AND "Team[Team]" = "{{CLOUD_ID}}" +``` + +- **Placeholders:** `{{RELEASE_VERSION}}`, `{{CLOUD_ID}}` +- **Notes:** Same as `code_freeze_issues` but scoped to a single team by Cloud ID. diff --git a/skills/rhdh-release/scripts/jql.py b/skills/rhdh-release/scripts/jql.py index 38aed60..dbd0075 100644 --- a/skills/rhdh-release/scripts/jql.py +++ b/skills/rhdh-release/scripts/jql.py @@ -73,6 +73,7 @@ def render( *, version: str | None = None, issue_type: str | None = None, + cloud_id: str | None = None, path: Path | None = None, ) -> str: """Render a JQL template with placeholder substitution.""" @@ -81,6 +82,8 @@ def render( jql = jql.replace("{{RELEASE_VERSION}}", version) if issue_type is not None: jql = jql.replace("{{ISSUE_TYPE}}", issue_type) + if cloud_id is not None: + jql = jql.replace("{{CLOUD_ID}}", cloud_id) return jql @@ -94,10 +97,11 @@ def render_with_url( *, version: str | None = None, issue_type: str | None = None, + cloud_id: str | None = None, path: Path | None = None, ) -> tuple[str, str]: """Render a JQL template and return (jql, jira_url).""" - jql = render(name, version=version, issue_type=issue_type, path=path) + jql = render(name, version=version, issue_type=issue_type, cloud_id=cloud_id, path=path) return jql, jira_url(jql) diff --git a/skills/rhdh-release/scripts/release.py b/skills/rhdh-release/scripts/release.py index 6fafd4c..87f20f2 100755 --- a/skills/rhdh-release/scripts/release.py +++ b/skills/rhdh-release/scripts/release.py @@ -619,39 +619,32 @@ def cmd_teams(args: argparse.Namespace, fmt: OutputFormatter) -> None: def cmd_team_breakdown(args: argparse.Namespace, fmt: OutputFormatter) -> None: - """Per-team issue counts for a release (requires parse_issues.py --enrich).""" + """Per-team issue counts for a release using JQL team filter.""" version = args.version - jql = jql_mod.render("open_issues", version=version) - issues = _acli_json_enriched(jql, select="key,summary,status,team") - teams = _fetch_teams(category="Engineering") - counts: dict[str, int] = {} - for issue in issues: - team = issue.get("team", "Unassigned") or "Unassigned" - norm = _normalize_team_name(team) - counts[norm] = counts.get(norm, 0) + 1 - rows = [] for t in teams: name = t["team_name"] - norm = _normalize_team_name(name) - count = counts.pop(norm, 0) + cid = t.get("cloud_id", "") + if not cid: + rows.append({"team_name": name, "cloud_id": "", "count": 0, + "leads": t.get("leads", ""), "slack_handles": t.get("slack_handles", [])}) + continue + jql, url = jql_mod.render_with_url("open_issues_by_team", version=version, cloud_id=cid) + count = _acli_count(jql, fmt) + fmt.log_info(f"{name:<25} {count:>5}") rows.append( { "team_name": name, - "team_id": t.get("team_id"), + "cloud_id": cid, "count": count, + "jira_url": url, "leads": t.get("leads", ""), "slack_handles": t.get("slack_handles", []), } ) - matched_norms = {_normalize_team_name(t["team_name"]) for t in teams} - for norm_name, count in sorted(counts.items()): - if norm_name not in matched_norms: - rows.append({"team_name": norm_name, "count": count}) - fmt.header(f"RHDH {version} — Issues by Team") for r in rows: 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 rn_jql, rn_url = jql_mod.render_with_url("release_notes", version=version) rn_count = _acli_count(rn_jql, fmt) - ff_jql = jql_mod.render("feature_freeze_issues", version=version) - issues = _acli_json_enriched(ff_jql, select="key,summary,status,team") - - team_counts: dict[str, int] = {} - for issue in issues: - team = issue.get("team", "Unassigned") or "Unassigned" - norm = _normalize_team_name(team) - team_counts[norm] = team_counts.get(norm, 0) + 1 - team_lines = [] for t in teams: name = t["team_name"] - norm = _normalize_team_name(name) - count = team_counts.get(norm, 0) + cid = t.get("cloud_id", "") + if not cid: + continue + jql, url = jql_mod.render_with_url( + "feature_freeze_issues_by_team", version=version, cloud_id=cid + ) + count = _acli_count(jql, fmt) slack_handles = t.get("slack_handles", []) lead_slack = slack_handles[0] if slack_handles else t.get("leads", "") team_lines.append( { "TEAM_NAME": name, "ISSUE_COUNT": str(count), - "JIRA_LINK": jql_mod.jira_url(ff_jql), + "JIRA_LINK": url, "LEAD_SLACK": lead_slack, } ) @@ -898,27 +887,23 @@ def cmd_slack_code_freeze_update(args: argparse.Namespace, fmt: OutputFormatter) fs_jql, fs_url = jql_mod.render_with_url("feature_subtasks", version=version) fs_count = _acli_count(fs_jql, fmt) - cf_jql = jql_mod.render("code_freeze_issues", version=version) - issues = _acli_json_enriched(cf_jql, select="key,summary,status,team") - - team_counts: dict[str, int] = {} - for issue in issues: - team = issue.get("team", "Unassigned") or "Unassigned" - norm = _normalize_team_name(team) - team_counts[norm] = team_counts.get(norm, 0) + 1 - team_lines = [] for t in teams: name = t["team_name"] - norm = _normalize_team_name(name) - count = team_counts.get(norm, 0) + cid = t.get("cloud_id", "") + if not cid: + continue + jql, url = jql_mod.render_with_url( + "code_freeze_issues_by_team", version=version, cloud_id=cid + ) + count = _acli_count(jql, fmt) slack_handles = t.get("slack_handles", []) lead_slack = slack_handles[0] if slack_handles else t.get("leads", "") team_lines.append( { "TEAM_NAME": name, "TEAM_ISSUE_COUNT": str(count), - "JIRA_LINK": jql_mod.jira_url(cf_jql), + "JIRA_LINK": url, "LEAD_SLACK": lead_slack, } ) diff --git a/skills/rhdh-release/workflows/issues-by-team.md b/skills/rhdh-release/workflows/issues-by-team.md index dee8043..a5c214a 100644 --- a/skills/rhdh-release/workflows/issues-by-team.md +++ b/skills/rhdh-release/workflows/issues-by-team.md @@ -27,23 +27,23 @@ If the CLI succeeds, use its output directly. If it fails, follow the manual ste gog sheets get 1vQXfvID72qwqvLb17eyGOvnZXrZG7NBzTGv6RP9wvyM Team --json --results-only ``` -Filter to category "Engineering" and status "Active". This gives team names and `team_id` values. +Filter to category "Engineering" and status "Active". This gives team names and `cloud_id` values. -## Step 3 (fallback): Query issues and filter by team +## Step 3 (fallback): Query issues by team using Cloud ID -Fetch all open issues for the release, enriched with team data: +Use the `open_issues_by_team` JQL template with the team's Cloud ID: ```bash -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 +acli jira workitem search --jql 'project IN (RHIDP, RHDHBugs, RHDHPLAN, RHDHSUPP) AND fixVersion = "{{RELEASE_VERSION}}" AND status != closed AND "Team[Team]" = "{{CLOUD_ID}}"' --count ``` -To filter to a specific team by team ID: +To get full issue details for a team: ```bash -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 +acli jira workitem search --jql 'project IN (RHIDP, RHDHBugs, RHDHPLAN, RHDHSUPP) AND fixVersion = "{{RELEASE_VERSION}}" AND status != closed AND "Team[Team]" = "{{CLOUD_ID}}"' --json ``` -**Important:** Always use `parse_issues.py --enrich` for team counts — the Team field is a custom field that cannot be queried via JQL directly. +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). ## Step 4 (fallback): Build per-team counts @@ -51,8 +51,8 @@ For each active engineering team, count the matching issues and build a Jira sea ## Step 5 (fallback): Format output -| Team | Team ID | Issue Count | Lead | Jira Link | -|------|---------|-------------|------|-----------| +| Team | Cloud ID | Issue Count | Lead | Jira Link | +|------|----------|-------------|------|-----------| | {{TEAM_NAME}} | {{TEAM_ID}} | {{COUNT}} | @{{LEAD_SLACK}} | [View](https://issues.redhat.com/issues/?jql=...) | diff --git a/skills/rhdh-release/workflows/teams-and-leads.md b/skills/rhdh-release/workflows/teams-and-leads.md index 847150b..fa5f831 100644 --- a/skills/rhdh-release/workflows/teams-and-leads.md +++ b/skills/rhdh-release/workflows/teams-and-leads.md @@ -51,7 +51,7 @@ Include link to source: [RHDH Team Mapping](https://docs.google.com/spreadsheets - By default only active teams are returned. The CLI filters by `status = Active` in the Google Sheet. -- 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. +- 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.