From d027f6100caa6cdd3d3f515b1105444e0cef704c Mon Sep 17 00:00:00 2001 From: John Strunk Date: Mon, 1 Jul 2024 15:58:11 +0000 Subject: [PATCH 1/3] Add issue categorization for status rollups Signed-off-by: John Strunk --- cfhelper.py | 14 ++++++++++- jira_howto.ipynb | 33 ++++++++++++++++++++++++ rollup_status.py | 65 ++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 100 insertions(+), 12 deletions(-) diff --git a/cfhelper.py b/cfhelper.py index 425912a..e2741cd 100644 --- a/cfhelper.py +++ b/cfhelper.py @@ -102,6 +102,17 @@ def add(self, content: int | str | ET.Element) -> "CFElement": >>> e = CFElement("p").add("Hello, ").add(CFElement("b", content="world")).add("!") >>> print(ET.tostring(e, encoding="unicode"))

Hello, world!

+ + # Simple text can be concatenated as well + >>> e = CFElement("p").add("Hello, ").add("world").add("!") + >>> print(ET.tostring(e, encoding="unicode")) +

Hello, world!

+ + # Multiple elements + >>> e = CFElement("p").add(CFElement("b", content="Hello")).add(", ") + >>> _ = e.add(CFElement("i", content="world")).add("!") + >>> print(ET.tostring(e, encoding="unicode")) +

Hello, world!

""" if isinstance(content, int): content = str(content) @@ -111,7 +122,8 @@ def add(self, content: int | str | ET.Element) -> "CFElement": self[-1].tail = self[-1].tail or "" self[-1].tail += content else: - self.text = content + self.text = self.text or "" + self.text += content else: self.append(content) return self diff --git a/jira_howto.ipynb b/jira_howto.ipynb index 1bf23e7..3aedb03 100644 --- a/jira_howto.ipynb +++ b/jira_howto.ipynb @@ -228,6 +228,39 @@ "\n", "print(f\"\\n\\nI am: {jiraissues.get_self(jira)}\")" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import summarizer\n", + "summarizer = importlib.reload(summarizer)\n", + "\n", + "i = jiraissues.issue_cache.get_issue(jira, \"OCTOET-77\")\n", + "print(summarizer.is_active(i, 14))\n", + "print(summarizer.is_active(i, 14, True))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from jiraissues import descendants, issue_cache\n", + "import rollup_status\n", + "rollup_status = importlib.reload(rollup_status)\n", + "\n", + "i = jiraissues.issue_cache.get_issue(jira, \"OCTOET-85\")\n", + "dkeys = descendants(jira, i.key)\n", + "print(dkeys)\n", + "\n", + "cats = rollup_status.categorize_issues(\n", + " {issue_cache.get_issue(jira, k) for k in dkeys}, 14)\n", + "pprint(cats)" + ] } ], "metadata": { diff --git a/rollup_status.py b/rollup_status.py index ed09795..d65909b 100755 --- a/rollup_status.py +++ b/rollup_status.py @@ -11,7 +11,7 @@ from atlassian import Confluence, Jira # type: ignore from cfhelper import CFElement, jiralink -from jiraissues import Issue, User, issue_cache +from jiraissues import Issue, User, descendants, issue_cache from simplestats import Timer from summarizer import get_chat_model, is_active, rollup_contributors, summarize_issue @@ -104,6 +104,33 @@ def element_contrib_list(header: str, contributors: set[User]) -> CFElement: ) +def categorize_issues(issues: set[Issue], inactive_days: int) -> dict[str, set[Issue]]: + """ + Categorize issues by status + + Parameters: + - issues: The set of issues to categorize + - inactive_days: Number of days before an issue is considered inactive + + Returns: + A dictionary of categorized issues + """ + categorized: dict[str, set[Issue]] = {} + + categorized["active"] = { + issue for issue in issues if is_active(issue, inactive_days, False) + } + categorized["inactive"] = { + issue for issue in issues if not is_active(issue, inactive_days, False) + } + categorized["closed"] = {issue for issue in issues if issue.status == "Closed"} + categorized["backlog"] = { + issue for issue in issues if issue.status in ["Backlog", "New", "ToDo"] + } + + return categorized + + def main() -> None: # pylint: disable=too-many-locals,too-many-statements """Main function""" # pylint: disable=duplicate-code @@ -144,8 +171,8 @@ def main() -> None: # pylint: disable=too-many-locals,too-many-statements stime.start() logging.info("Collecting issue summaries for children of %s", issue_key) child_inputs: list[IssueSummary] = [] - epic = issue_cache.get_issue(jclient, issue_key) - for child in epic.children: + initiative = issue_cache.get_issue(jclient, issue_key) + for child in initiative.children: issue = issue_cache.get_issue(jclient, child.key) if not is_active(issue, inactive_days, True): logging.info("Skipping inactive issue %s", issue.key) @@ -196,20 +223,15 @@ def main() -> None: # pylint: disable=too-many-locals,too-many-statements """ exec_paragraph = textwrap.fill(llm.invoke(prompt, stop=["<|endoftext|>"]).strip()) - # Generate the overall status update - parent_page_id = lookup_page(cclient, args.parent) - - page_title = f"Initiative status: {epic.key} - {epic.summary}" - # Root element for the page; tag doesn't matter as it will be stripped off later page = CFElement("root") # Top of the page; overall executive summary and initiative contributors page.add(CFElement("h1", content="Executive Summary")) - page.add(CFElement("p", content=jiralink(epic.key))) + page.add(CFElement("p", content=jiralink(initiative.key))) page.add(CFElement("p", content=exec_paragraph)) - contributors = rollup_contributors(epic) - active_contributors = rollup_contributors(epic, active_days=inactive_days) + contributors = rollup_contributors(initiative) + active_contributors = rollup_contributors(initiative, active_days=inactive_days) if active_contributors: page.add(element_contrib_list("Active contributors", active_contributors)) if contributors: @@ -229,6 +251,27 @@ def main() -> None: # pylint: disable=too-many-locals,too-many-statements if item.contributors: page.add(element_contrib_list("All contributors", item.contributors)) + # Create counts for all descendant issues of the current epic issue + desc_keys = descendants(jclient, issue.key) + cats = categorize_issues( + {issue_cache.get_issue(jclient, key) for key in desc_keys}, + inactive_days, + ) + d_tag = CFElement("p", content=CFElement("b", content="Sub-issues: ")) + counts: list[str] = [] + if cats["active"]: + counts.append(f"{len(cats['active'])}(Active)") + if cats["closed"]: + counts.append(f"{len(cats['closed'])}(Closed)") + if cats["backlog"]: + counts.append(f"{len(cats['backlog'])}(Backlog)") + d_tag.add(", ".join(counts)) + d_tag.add(f" — Total {len(desc_keys)}") + page.add(d_tag) + + # Post the page to Confluence + parent_page_id = lookup_page(cclient, args.parent) + page_title = f"Initiative status: {initiative.key} - {initiative.summary}" cclient.update_or_create(parent_page_id, page_title, page.unwrap()) From e14cf4ab2bc703a1f0a974e4b37e6390db543315 Mon Sep 17 00:00:00 2001 From: John Strunk Date: Mon, 1 Jul 2024 16:06:34 +0000 Subject: [PATCH 2/3] Add CronJob to automatically update rollup status Signed-off-by: John Strunk --- README.md | 3 + rollup.yaml | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 rollup.yaml diff --git a/README.md b/README.md index 1cce38a..d1bdb6c 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ The following variables are required: - `ALLOWED_PROJECTS`: A comma-separated list of Jira project keys that the bot is allowed to summarize (e.g., `ALLOWED_PROJECTS=OCTO,OCTOET`) (for the bot) +- `CONFLUENCE_TOKEN`: A Confluence PAT token that will allow updating the + Confluence pages with the summaries (for `rollup_status.py`) +- `CONFLUENCE_URL`: The URL for the Confluence instance (e.g., `https://...`) - `GENAI_API`: The API endpoint for the IBM AI model (e.g., `https://...`) - `GENAI_KEY`: Your API key for the IBM AI model - `JIRA_TOKEN`: A JIRA PAT token that will allow retrieving issues from Jira as diff --git a/rollup.yaml b/rollup.yaml new file mode 100644 index 0000000..29c0308 --- /dev/null +++ b/rollup.yaml @@ -0,0 +1,166 @@ +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: initiative-rollups +spec: + schedule: "0 5 * * 1" + jobTemplate: + spec: + template: + spec: + containers: + ################################################## + ## OCTO-1 + - name: rollup-1 + image: ghcr.io/johnstrunk/jira-summarizer:latest + command: + - "/app/.venv/bin/python" + - "rollup-status.py" + args: + - "--log-level" + - "INFO" + - "--parent" + - "Initiative summaries" + - OCTO-1 + envFrom: + - secretRef: + name: jira-summarizer-secret + optional: false + resources: + requests: + memory: "64Mi" + cpu: "10m" + limits: + memory: "128Mi" + cpu: "1000m" + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + ################################################## + ## OCTO-2 + - name: rollup-2 + image: ghcr.io/johnstrunk/jira-summarizer:latest + command: + - "/app/.venv/bin/python" + - "rollup-status.py" + args: + - "--log-level" + - "INFO" + - "--parent" + - "Initiative summaries" + - OCTO-2 + envFrom: + - secretRef: + name: jira-summarizer-secret + optional: false + resources: + requests: + memory: "64Mi" + cpu: "10m" + limits: + memory: "128Mi" + cpu: "1000m" + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + ################################################## + ## OCTO-3 + - name: rollup-3 + image: ghcr.io/johnstrunk/jira-summarizer:latest + command: + - "/app/.venv/bin/python" + - "rollup-status.py" + args: + - "--log-level" + - "INFO" + - "--parent" + - "Initiative summaries" + - OCTO-3 + envFrom: + - secretRef: + name: jira-summarizer-secret + optional: false + resources: + requests: + memory: "64Mi" + cpu: "10m" + limits: + memory: "128Mi" + cpu: "1000m" + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + ################################################## + ## OCTO-4 + - name: rollup-4 + image: ghcr.io/johnstrunk/jira-summarizer:latest + command: + - "/app/.venv/bin/python" + - "rollup-status.py" + args: + - "--log-level" + - "INFO" + - "--parent" + - "Initiative summaries" + - OCTO-4 + envFrom: + - secretRef: + name: jira-summarizer-secret + optional: false + resources: + requests: + memory: "64Mi" + cpu: "10m" + limits: + memory: "128Mi" + cpu: "1000m" + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + ################################################## + ## OCTO-6 + - name: rollup-6 + image: ghcr.io/johnstrunk/jira-summarizer:latest + command: + - "/app/.venv/bin/python" + - "rollup-status.py" + args: + - "--log-level" + - "INFO" + - "--parent" + - "Initiative summaries" + - OCTO-6 + envFrom: + - secretRef: + name: jira-summarizer-secret + optional: false + resources: + requests: + memory: "64Mi" + cpu: "10m" + limits: + memory: "128Mi" + cpu: "1000m" + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + securityContext: + runAsNonRoot: true + terminationGracePeriodSeconds: 10 + restartPolicy: OnFailure From 65047e9c4ae9a3bbd6feb6cd8319b3ff032a795d Mon Sep 17 00:00:00 2001 From: John Strunk Date: Mon, 1 Jul 2024 16:11:29 +0000 Subject: [PATCH 3/3] Adjust Summary API Deployment to use RollingUpdate strategy Signed-off-by: John Strunk --- summarize-api.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/summarize-api.yaml b/summarize-api.yaml index 3b8fb76..f47f3cf 100644 --- a/summarize-api.yaml +++ b/summarize-api.yaml @@ -44,7 +44,7 @@ spec: matchLabels: app: summarize-api strategy: - type: Recreate + type: RollingUpdate template: metadata: labels: @@ -85,7 +85,7 @@ spec: mountPath: /tmp securityContext: runAsNonRoot: true - terminationGracePeriodSeconds: 10 + terminationGracePeriodSeconds: 30 volumes: - name: tmp-volume emptyDir: {}