chore: import upstream snapshot with attribution
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: github-workflow
|
||||
description: Use GitHub workflow tools to read work status, draft reports, summarize follow-ups, and execute only approved issue mutations.
|
||||
tools:
|
||||
- list_github_work_items
|
||||
- summarize_github_pr_status
|
||||
- list_github_security_alerts
|
||||
- generate_work_status_report
|
||||
- summarize_community_followups
|
||||
- propose_github_issue_mutation_from_slack
|
||||
- execute_github_issue_mutation
|
||||
---
|
||||
|
||||
# GitHub Workflow
|
||||
|
||||
Use this workflow when the user asks about GitHub engineering status, PR readiness,
|
||||
community follow-ups, or turning an explicit Slack request into a GitHub issue.
|
||||
|
||||
1. Read before reporting. Use the read-only GitHub tools first, and treat missing
|
||||
or failed reads as incomplete status rather than proof that there are no
|
||||
blockers.
|
||||
2. Report or summarize from known data. Use `generate_work_status_report` for
|
||||
Slack-ready status and `summarize_community_followups` for unanswered
|
||||
contributor questions or agenda items.
|
||||
3. Propose mutations before execution. Use
|
||||
`propose_github_issue_mutation_from_slack` only for explicit Slack-sourced
|
||||
create, update, or close requests.
|
||||
4. Execute only through approval. `execute_github_issue_mutation` is the only
|
||||
mutating GitHub workflow tool. It is never an investigation action and must
|
||||
remain chat-only, approval-gated, and proposal-driven.
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Semantic helpers for GitHub workflow tools."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from integrations.github.client import GitHubApiError, GitHubRestClient
|
||||
from integrations.github.tools.workflow.followup import (
|
||||
issue_number_from_url,
|
||||
normalize_community_comment,
|
||||
summarize_community_followups_from_comments,
|
||||
)
|
||||
from integrations.github.tools.workflow.models import (
|
||||
CommunityFollowup,
|
||||
GitHubIssueMutationProposal,
|
||||
GitHubReadSnapshot,
|
||||
IssueMutationOperation,
|
||||
PullRequestStatus,
|
||||
SecurityAlert,
|
||||
WorkItem,
|
||||
WorkStatusReport,
|
||||
)
|
||||
from integrations.github.tools.workflow.mutation import (
|
||||
build_issue_mutation_proposal,
|
||||
title_from_slack_text,
|
||||
)
|
||||
from integrations.github.tools.workflow.report import build_work_status_report
|
||||
|
||||
__all__ = [
|
||||
"CommunityFollowup",
|
||||
"GitHubApiError",
|
||||
"GitHubIssueMutationProposal",
|
||||
"GitHubReadSnapshot",
|
||||
"GitHubRestClient",
|
||||
"IssueMutationOperation",
|
||||
"PullRequestStatus",
|
||||
"SecurityAlert",
|
||||
"WorkItem",
|
||||
"WorkStatusReport",
|
||||
"build_issue_mutation_proposal",
|
||||
"build_work_status_report",
|
||||
"issue_number_from_url",
|
||||
"normalize_community_comment",
|
||||
"summarize_community_followups_from_comments",
|
||||
"title_from_slack_text",
|
||||
]
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Community follow-up summarization for GitHub workflow tools."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from integrations.github.tools.workflow.models import CommunityFollowup
|
||||
|
||||
_ISSUE_NUMBER_RE = re.compile(r"/issues/(?P<number>\d+)(?:$|[#?])")
|
||||
|
||||
|
||||
def issue_number_from_url(url: str) -> int | None:
|
||||
match = _ISSUE_NUMBER_RE.search(url)
|
||||
if match is None:
|
||||
return None
|
||||
return int(match.group("number"))
|
||||
|
||||
|
||||
def normalize_community_comment(raw: dict[str, Any]) -> CommunityFollowup:
|
||||
"""Normalize GitHub repository issue-comment payloads."""
|
||||
|
||||
issue_number = raw.get("issue_number")
|
||||
if not isinstance(issue_number, int):
|
||||
issue_url = str(raw.get("issue_url") or raw.get("html_url") or "")
|
||||
issue_number = issue_number_from_url(issue_url)
|
||||
return CommunityFollowup(
|
||||
issue_number=issue_number,
|
||||
issue_title=str(raw.get("issue_title", "")),
|
||||
author=str(raw.get("author") or (raw.get("user") or {}).get("login", "")),
|
||||
body=str(raw.get("body", "")),
|
||||
created_at=str(raw.get("created_at", "")),
|
||||
url=str(raw.get("url") or raw.get("html_url") or ""),
|
||||
)
|
||||
|
||||
|
||||
def _is_question(text: str) -> bool:
|
||||
lowered = text.lower()
|
||||
return "?" in text or lowered.startswith(
|
||||
("when ", "what ", "who ", "how ", "where ", "can ", "could ")
|
||||
)
|
||||
|
||||
|
||||
def _is_agenda_item(text: str) -> bool:
|
||||
lowered = text.lower()
|
||||
return any(term in lowered for term in ("agenda", "standup"))
|
||||
|
||||
|
||||
def _question_answered_later(
|
||||
question: CommunityFollowup,
|
||||
comments: list[CommunityFollowup],
|
||||
maintainers: set[str],
|
||||
) -> bool:
|
||||
for comment in comments:
|
||||
if comment.issue_number != question.issue_number:
|
||||
continue
|
||||
if comment.created_at <= question.created_at:
|
||||
continue
|
||||
if comment.author.lower() in maintainers:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _suggest_reply(question: CommunityFollowup) -> dict[str, Any]:
|
||||
return {
|
||||
"issue_number": question.issue_number,
|
||||
"issue_title": question.issue_title,
|
||||
"context": question.body,
|
||||
"suggested_reply": (
|
||||
"Thanks for the question — we should confirm the current owner/status "
|
||||
"and reply in this thread with the next concrete step."
|
||||
),
|
||||
"url": question.url,
|
||||
}
|
||||
|
||||
|
||||
def summarize_community_followups_from_comments(
|
||||
*,
|
||||
comments: list[dict[str, Any]],
|
||||
maintainer_logins: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
normalized_comments = [normalize_community_comment(comment) for comment in comments]
|
||||
maintainers = {login.lower() for login in (maintainer_logins or [])}
|
||||
questions = [comment for comment in normalized_comments if _is_question(comment.body)]
|
||||
unanswered = [
|
||||
question
|
||||
for question in questions
|
||||
if question.author.lower() not in maintainers
|
||||
and not _question_answered_later(question, normalized_comments, maintainers)
|
||||
]
|
||||
agenda_items = [comment for comment in normalized_comments if _is_agenda_item(comment.body)]
|
||||
return {
|
||||
"unanswered_questions": [question.to_dict() for question in unanswered],
|
||||
"agenda_items": [item.to_dict() for item in agenda_items],
|
||||
"suggested_replies": [_suggest_reply(question) for question in unanswered],
|
||||
"counts": {
|
||||
"comments": len(normalized_comments),
|
||||
"unanswered_questions": len(unanswered),
|
||||
"agenda_items": len(agenda_items),
|
||||
},
|
||||
"side_effects": [],
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
"""Semantic GitHub workflow models."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Literal
|
||||
|
||||
IssueMutationOperation = Literal["create", "update", "close"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WorkItem:
|
||||
number: int | None
|
||||
title: str
|
||||
state: str
|
||||
url: str
|
||||
author: str
|
||||
labels: list[str]
|
||||
assignees: list[str]
|
||||
updated_at: str
|
||||
work_status: Literal["taken", "up_for_grabs", "unassigned"]
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"number": self.number,
|
||||
"title": self.title,
|
||||
"state": self.state,
|
||||
"url": self.url,
|
||||
"author": self.author,
|
||||
"labels": self.labels,
|
||||
"assignees": self.assignees,
|
||||
"updated_at": self.updated_at,
|
||||
"work_status": self.work_status,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PullRequestStatus:
|
||||
number: int | None
|
||||
title: str
|
||||
url: str
|
||||
author: str
|
||||
head_ref: str
|
||||
head_sha: str
|
||||
draft: bool
|
||||
mergeable: bool | None
|
||||
mergeable_state: str
|
||||
check_status: str
|
||||
status: Literal["mergeable", "blocked", "unknown"]
|
||||
mergeability: Literal["mergeable", "blocked", "unknown"]
|
||||
blocking_reasons: list[str]
|
||||
updated_at: str
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"number": self.number,
|
||||
"title": self.title,
|
||||
"url": self.url,
|
||||
"author": self.author,
|
||||
"head_ref": self.head_ref,
|
||||
"head_sha": self.head_sha,
|
||||
"draft": self.draft,
|
||||
"mergeable": self.mergeable,
|
||||
"mergeable_state": self.mergeable_state,
|
||||
"check_status": self.check_status,
|
||||
"status": self.status,
|
||||
"mergeability": self.mergeability,
|
||||
"blocking_reasons": self.blocking_reasons,
|
||||
"updated_at": self.updated_at,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SecurityAlert:
|
||||
type: str
|
||||
number: Any
|
||||
state: str
|
||||
summary: str
|
||||
url: str
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": self.type,
|
||||
"number": self.number,
|
||||
"state": self.state,
|
||||
"summary": self.summary,
|
||||
"url": self.url,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CommunityFollowup:
|
||||
issue_number: int | None
|
||||
issue_title: str
|
||||
author: str
|
||||
body: str
|
||||
created_at: str
|
||||
url: str
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"issue_number": self.issue_number,
|
||||
"issue_title": self.issue_title,
|
||||
"author": self.author,
|
||||
"body": self.body,
|
||||
"created_at": self.created_at,
|
||||
"url": self.url,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GitHubReadSnapshot:
|
||||
owner: str
|
||||
repo: str
|
||||
work_items: list[WorkItem] = field(default_factory=list)
|
||||
pull_requests: list[PullRequestStatus] = field(default_factory=list)
|
||||
security_alerts: list[SecurityAlert] = field(default_factory=list)
|
||||
errors: list[str] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def incomplete(self) -> bool:
|
||||
return bool(self.errors)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WorkStatusReport:
|
||||
counts: dict[str, int]
|
||||
slack_markdown: str
|
||||
available: bool
|
||||
incomplete: bool
|
||||
errors: list[str]
|
||||
side_effects: list[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"counts": self.counts,
|
||||
"slack_markdown": self.slack_markdown,
|
||||
"available": self.available,
|
||||
"incomplete": self.incomplete,
|
||||
"errors": self.errors,
|
||||
"side_effects": self.side_effects,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GitHubIssueMutationProposal:
|
||||
proposal_id: str
|
||||
operation: IssueMutationOperation
|
||||
owner: str
|
||||
repo: str
|
||||
target: dict[str, Any]
|
||||
payload: dict[str, Any]
|
||||
slack_url: str
|
||||
idempotency_marker: str
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"proposal_id": self.proposal_id,
|
||||
"operation": self.operation,
|
||||
"owner": self.owner,
|
||||
"repo": self.repo,
|
||||
"target": self.target,
|
||||
"payload": self.payload,
|
||||
"slack_url": self.slack_url,
|
||||
"idempotency_marker": self.idempotency_marker,
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Mutation proposal helpers for GitHub issue workflow tools."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from integrations.github.tools.workflow.models import (
|
||||
GitHubIssueMutationProposal,
|
||||
IssueMutationOperation,
|
||||
)
|
||||
|
||||
|
||||
def _proposal_digest(data: dict[str, Any]) -> str:
|
||||
raw = json.dumps(data, sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]
|
||||
|
||||
|
||||
def _issue_body(slack_text: str, slack_url: str, marker: str) -> str:
|
||||
body = ["## Slack request", "", slack_text.strip() or "(No Slack text provided.)"]
|
||||
if slack_url.strip():
|
||||
body.extend(["", f"Source: {slack_url.strip()}"])
|
||||
body.extend(["", f"<!-- {marker} -->"])
|
||||
return "\n".join(body)
|
||||
|
||||
|
||||
def _comment_body(slack_text: str, slack_url: str, marker: str) -> str:
|
||||
body = ["Slack follow-up:", "", slack_text.strip() or "(No Slack text provided.)"]
|
||||
if slack_url.strip():
|
||||
body.extend(["", f"Source: {slack_url.strip()}"])
|
||||
body.extend(["", f"<!-- {marker} -->"])
|
||||
return "\n".join(body)
|
||||
|
||||
|
||||
def title_from_slack_text(slack_text: str) -> str:
|
||||
cleaned = " ".join(slack_text.strip().split())
|
||||
if not cleaned:
|
||||
return "Task from Slack"
|
||||
return cleaned[:80].rstrip(" .")
|
||||
|
||||
|
||||
def build_issue_mutation_proposal(
|
||||
*,
|
||||
owner: str,
|
||||
repo: str,
|
||||
operation: IssueMutationOperation,
|
||||
slack_text: str,
|
||||
slack_url: str = "",
|
||||
issue_number: int | None = None,
|
||||
title: str = "",
|
||||
labels: list[str] | None = None,
|
||||
assignees: list[str] | None = None,
|
||||
) -> GitHubIssueMutationProposal:
|
||||
seed = {
|
||||
"owner": owner,
|
||||
"repo": repo,
|
||||
"operation": operation,
|
||||
"issue_number": issue_number,
|
||||
"slack_text": " ".join(slack_text.split()),
|
||||
"slack_url": slack_url.strip(),
|
||||
"title": title.strip(),
|
||||
"labels": labels or [],
|
||||
"assignees": assignees or [],
|
||||
}
|
||||
proposal_id = f"ghslack-{_proposal_digest(seed)}"
|
||||
marker = f"opensre-slack-proposal:{proposal_id}"
|
||||
target = {"issue_number": issue_number} if issue_number is not None else {}
|
||||
|
||||
if operation == "create":
|
||||
payload: dict[str, Any] = {
|
||||
"title": title.strip() or title_from_slack_text(slack_text),
|
||||
"body": _issue_body(slack_text, slack_url, marker),
|
||||
}
|
||||
if labels is not None:
|
||||
payload["labels"] = labels
|
||||
if assignees is not None:
|
||||
payload["assignees"] = assignees
|
||||
elif operation == "update":
|
||||
payload = {"comment_body": _comment_body(slack_text, slack_url, marker)}
|
||||
if title.strip():
|
||||
payload["title"] = title.strip()
|
||||
if labels is not None:
|
||||
payload["labels"] = labels
|
||||
if assignees is not None:
|
||||
payload["assignees"] = assignees
|
||||
else:
|
||||
payload = {
|
||||
"comment_body": _comment_body(slack_text, slack_url, marker),
|
||||
"state": "closed",
|
||||
"state_reason": "completed",
|
||||
}
|
||||
|
||||
return GitHubIssueMutationProposal(
|
||||
proposal_id=proposal_id,
|
||||
operation=operation,
|
||||
owner=owner,
|
||||
repo=repo,
|
||||
target=target,
|
||||
payload=payload,
|
||||
slack_url=slack_url.strip(),
|
||||
idempotency_marker=marker,
|
||||
)
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Report composition helpers for GitHub workflow tools."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from integrations.github.tools.workflow.models import PullRequestStatus, WorkItem, WorkStatusReport
|
||||
|
||||
|
||||
def _as_dict(item: WorkItem | PullRequestStatus | dict[str, Any]) -> dict[str, Any]:
|
||||
if isinstance(item, WorkItem | PullRequestStatus):
|
||||
return item.to_dict()
|
||||
return item
|
||||
|
||||
|
||||
def _item_line(item: dict[str, Any]) -> str:
|
||||
assignees = item.get("assignees") or []
|
||||
owner = f" — @{', @'.join(assignees)}" if assignees else ""
|
||||
return f"• #{item.get('number', '?')} {item.get('title', '')}{owner}"
|
||||
|
||||
|
||||
def _pr_line(pr: dict[str, Any]) -> str:
|
||||
reasons = pr.get("blocking_reasons") or []
|
||||
reason_text = f" — {', '.join(str(reason) for reason in reasons)}" if reasons else ""
|
||||
return f"• PR #{pr.get('number', '?')} {pr.get('title', '')}{reason_text}"
|
||||
|
||||
|
||||
def _recommended_actions(
|
||||
*,
|
||||
up_for_grabs: list[dict[str, Any]],
|
||||
unassigned: list[dict[str, Any]],
|
||||
blocked_prs: list[dict[str, Any]],
|
||||
mergeable_prs: list[dict[str, Any]],
|
||||
errors: list[str],
|
||||
) -> list[str]:
|
||||
actions: list[str] = []
|
||||
if errors:
|
||||
actions.append("• Re-run failed GitHub reads before trusting this status report.")
|
||||
if blocked_prs:
|
||||
actions.append(f"• Unblock {len(blocked_prs)} PR(s) before starting new work.")
|
||||
if mergeable_prs:
|
||||
actions.append(f"• Review or merge {len(mergeable_prs)} ready PR(s).")
|
||||
if up_for_grabs:
|
||||
actions.append(f"• Assign {len(up_for_grabs)} up-for-grabs task(s).")
|
||||
if unassigned:
|
||||
actions.append(f"• Triage {len(unassigned)} unassigned issue(s).")
|
||||
if not actions:
|
||||
actions.append("• No obvious blockers from the supplied data.")
|
||||
return actions
|
||||
|
||||
|
||||
def build_work_status_report(
|
||||
*,
|
||||
work_items: Sequence[WorkItem | dict[str, Any]],
|
||||
pull_requests: Sequence[PullRequestStatus | dict[str, Any]],
|
||||
context: str = "today",
|
||||
errors: list[str] | None = None,
|
||||
) -> WorkStatusReport:
|
||||
"""Build a Slack-ready report from an already-read GitHub snapshot."""
|
||||
|
||||
errors = list(errors or [])
|
||||
item_dicts = [_as_dict(item) for item in work_items]
|
||||
pr_dicts = [_as_dict(pr) for pr in pull_requests]
|
||||
up_for_grabs = [item for item in item_dicts if item.get("work_status") == "up_for_grabs"]
|
||||
unassigned = [item for item in item_dicts if item.get("work_status") == "unassigned"]
|
||||
taken = [item for item in item_dicts if item.get("work_status") == "taken"]
|
||||
blocked_prs = [pr for pr in pr_dicts if pr.get("status") == "blocked"]
|
||||
mergeable_prs = [pr for pr in pr_dicts if pr.get("status") == "mergeable"]
|
||||
unknown_prs = [pr for pr in pr_dicts if pr.get("status") == "unknown"]
|
||||
|
||||
sections = [f"*Engineering status — {context}*", ""]
|
||||
if errors:
|
||||
sections.extend(["*Incomplete report:*", *[f"• {error}" for error in errors], ""])
|
||||
sections.append(
|
||||
f"*Open work:* {len(item_dicts)} total ({len(taken)} taken, "
|
||||
f"{len(up_for_grabs)} up for grabs, {len(unassigned)} unassigned)"
|
||||
)
|
||||
if up_for_grabs:
|
||||
sections.extend(["", "*Up for grabs:*", *[_item_line(item) for item in up_for_grabs[:10]]])
|
||||
if unassigned:
|
||||
sections.extend(["", "*Unassigned:*", *[_item_line(item) for item in unassigned[:10]]])
|
||||
if blocked_prs:
|
||||
sections.extend(["", "*Blocked PRs:*", *[_pr_line(pr) for pr in blocked_prs[:10]]])
|
||||
if unknown_prs:
|
||||
sections.extend(["", "*Unknown PR status:*", *[_pr_line(pr) for pr in unknown_prs[:10]]])
|
||||
if mergeable_prs:
|
||||
sections.extend(["", "*Ready to merge:*", *[_pr_line(pr) for pr in mergeable_prs[:10]]])
|
||||
sections.extend(
|
||||
[
|
||||
"",
|
||||
"*Recommended next actions:*",
|
||||
*_recommended_actions(
|
||||
up_for_grabs=up_for_grabs,
|
||||
unassigned=unassigned,
|
||||
blocked_prs=blocked_prs,
|
||||
mergeable_prs=mergeable_prs,
|
||||
errors=errors,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
return WorkStatusReport(
|
||||
counts={
|
||||
"open_work": len(item_dicts),
|
||||
"taken": len(taken),
|
||||
"up_for_grabs": len(up_for_grabs),
|
||||
"unassigned": len(unassigned),
|
||||
"blocked_prs": len(blocked_prs),
|
||||
"mergeable_prs": len(mergeable_prs),
|
||||
"unknown_prs": len(unknown_prs),
|
||||
},
|
||||
slack_markdown="\n".join(sections).strip(),
|
||||
available=not errors,
|
||||
incomplete=bool(errors),
|
||||
errors=errors,
|
||||
)
|
||||
Reference in New Issue
Block a user