Skip to content

Commit 094f66e

Browse files
authored
Add async weekly standup automation (#1346)
2 parents 2de3431 + 303cca8 commit 094f66e

4 files changed

Lines changed: 266 additions & 0 deletions

File tree

.github/scripts/compile_standup.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#!/usr/bin/env python3
2+
"""Compile standup responses into a GitHub Discussion and close the issue."""
3+
4+
import os
5+
from datetime import datetime, timezone, timedelta
6+
7+
import requests
8+
9+
REPO = os.environ["GITHUB_REPOSITORY"]
10+
TOKEN = os.environ["STANDUP_TOKEN"]
11+
CATEGORY_NODE_ID = os.environ["DISCUSSION_CATEGORY_NODE_ID"]
12+
API = "https://api.github.com"
13+
GRAPHQL = "https://api.github.com/graphql"
14+
HEADERS = {
15+
"Authorization": f"token {TOKEN}",
16+
"Accept": "application/vnd.github+json",
17+
}
18+
19+
20+
def find_standup_issue():
21+
"""Find the most recent open standup-input issue from the last 7 days."""
22+
since = (datetime.now(timezone.utc) - timedelta(days=7)).isoformat()
23+
resp = requests.get(
24+
f"{API}/repos/{REPO}/issues",
25+
headers=HEADERS,
26+
params={
27+
"labels": "standup-input",
28+
"state": "open",
29+
"since": since,
30+
"sort": "created",
31+
"direction": "desc",
32+
"per_page": 1,
33+
},
34+
)
35+
resp.raise_for_status()
36+
issues = resp.json()
37+
if not issues:
38+
print("No standup-input issue found in the last 7 days.")
39+
return None
40+
return issues[0]
41+
42+
43+
def fetch_comments(issue_number):
44+
"""Fetch all comments on an issue."""
45+
comments = []
46+
page = 1
47+
while True:
48+
resp = requests.get(
49+
f"{API}/repos/{REPO}/issues/{issue_number}/comments",
50+
headers=HEADERS,
51+
params={"per_page": 100, "page": page},
52+
)
53+
resp.raise_for_status()
54+
batch = resp.json()
55+
if not batch:
56+
break
57+
comments.extend(batch)
58+
page += 1
59+
return comments
60+
61+
62+
def get_repo_node_id():
63+
"""Get the repository node ID for the GraphQL mutation."""
64+
resp = requests.get(f"{API}/repos/{REPO}", headers=HEADERS)
65+
resp.raise_for_status()
66+
return resp.json()["node_id"]
67+
68+
69+
def create_discussion(title, body, repo_node_id):
70+
"""Create a GitHub Discussion via GraphQL."""
71+
mutation = """
72+
mutation($repoId: ID!, $categoryId: ID!, $title: String!, $body: String!) {
73+
createDiscussion(input: {
74+
repositoryId: $repoId,
75+
categoryId: $categoryId,
76+
title: $title,
77+
body: $body
78+
}) {
79+
discussion {
80+
url
81+
}
82+
}
83+
}
84+
"""
85+
resp = requests.post(
86+
GRAPHQL,
87+
headers=HEADERS,
88+
json={
89+
"query": mutation,
90+
"variables": {
91+
"repoId": repo_node_id,
92+
"categoryId": CATEGORY_NODE_ID,
93+
"title": title,
94+
"body": body,
95+
},
96+
},
97+
)
98+
resp.raise_for_status()
99+
data = resp.json()
100+
if "errors" in data:
101+
raise RuntimeError(f"GraphQL errors: {data['errors']}")
102+
return data["data"]["createDiscussion"]["discussion"]["url"]
103+
104+
105+
def close_issue(issue_number, discussion_url):
106+
"""Close the standup issue with a link to the compiled discussion."""
107+
requests.post(
108+
f"{API}/repos/{REPO}/issues/{issue_number}/comments",
109+
headers=HEADERS,
110+
json={"body": f"Compiled into discussion: {discussion_url}"},
111+
)
112+
requests.patch(
113+
f"{API}/repos/{REPO}/issues/{issue_number}",
114+
headers=HEADERS,
115+
json={"state": "closed"},
116+
)
117+
118+
119+
def main():
120+
issue = find_standup_issue()
121+
if not issue:
122+
return
123+
124+
issue_number = issue["number"]
125+
# Extract the week label from the issue title
126+
title_suffix = issue["title"].removeprefix("Standup Input: ")
127+
week_label = title_suffix or datetime.now(timezone.utc).strftime("Week of %Y-%m-%d")
128+
129+
comments = fetch_comments(issue_number)
130+
131+
# Build sections per contributor
132+
sections = []
133+
for comment in comments:
134+
user = comment["user"]["login"]
135+
if comment["user"]["type"] == "Bot":
136+
continue
137+
body = comment["body"].strip()
138+
sections.append(f"### @{user}\n{body}")
139+
140+
updates = "\n\n".join(sections) if sections else "_No responses._"
141+
142+
discussion_title = f"Weekly Check-in: {week_label}"
143+
discussion_body = updates
144+
145+
repo_node_id = get_repo_node_id()
146+
discussion_url = create_discussion(discussion_title, discussion_body, repo_node_id)
147+
print(f"Created discussion: {discussion_url}")
148+
149+
close_issue(issue_number, discussion_url)
150+
print(f"Closed issue #{issue_number}")
151+
152+
153+
if __name__ == "__main__":
154+
main()
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env python3
2+
"""Create a weekly standup input issue and ping contributors."""
3+
4+
import os
5+
from datetime import datetime, timezone
6+
7+
import requests
8+
9+
REPO = os.environ["GITHUB_REPOSITORY"]
10+
TOKEN = os.environ["STANDUP_TOKEN"]
11+
API = "https://api.github.com"
12+
HEADERS = {
13+
"Authorization": f"token {TOKEN}",
14+
"Accept": "application/vnd.github+json",
15+
}
16+
17+
CONTRIBUTORS = [
18+
"DanGould",
19+
"spacebear21",
20+
"arminsabouri",
21+
"benalleng",
22+
"chavic",
23+
"zealsham",
24+
"Mshehu5",
25+
]
26+
27+
28+
def main():
29+
today = datetime.now(timezone.utc)
30+
week_label = today.strftime("%Y-%m-%d")
31+
title = f"Standup Input: Week of {week_label}"
32+
33+
cc_line = " ".join(f"@{u}" for u in CONTRIBUTORS)
34+
body = (
35+
"Please reply by **Monday end-of-day** (your timezone).\n\n"
36+
"Format:\n"
37+
"- **Shipped**: What you landed last week (PR/issue links)\n"
38+
"- **Focus**: What you're working on this week\n"
39+
"- **Blockers**: Anything stopping you — name who can help\n\n"
40+
f"cc {cc_line}"
41+
)
42+
43+
# Ensure the label exists
44+
label_url = f"{API}/repos/{REPO}/labels/standup-input"
45+
resp = requests.get(label_url, headers=HEADERS)
46+
if resp.status_code == 404:
47+
requests.post(
48+
f"{API}/repos/{REPO}/labels",
49+
headers=HEADERS,
50+
json={
51+
"name": "standup-input",
52+
"color": "0E8A16",
53+
"description": "Weekly standup input issue",
54+
},
55+
)
56+
57+
# Create the issue
58+
resp = requests.post(
59+
f"{API}/repos/{REPO}/issues",
60+
headers=HEADERS,
61+
json={"title": title, "body": body, "labels": ["standup-input"]},
62+
)
63+
resp.raise_for_status()
64+
issue = resp.json()
65+
print(f"Created issue #{issue['number']}: {issue['html_url']}")
66+
67+
68+
if __name__ == "__main__":
69+
main()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Standup Compile
2+
3+
on:
4+
schedule:
5+
# Tuesday 06:00 UTC = Tuesday 14:00 Taipei
6+
- cron: "0 6 * * 2"
7+
workflow_dispatch:
8+
9+
jobs:
10+
compile:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-python@v5
15+
with:
16+
python-version: "3.12"
17+
- run: pip install requests
18+
- run: python .github/scripts/compile_standup.py
19+
env:
20+
GITHUB_REPOSITORY: ${{ github.repository }}
21+
STANDUP_TOKEN: ${{ secrets.STANDUP_TOKEN }}
22+
DISCUSSION_CATEGORY_NODE_ID: ${{ secrets.DISCUSSION_CATEGORY_NODE_ID }}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Standup Prompt
2+
3+
on:
4+
schedule:
5+
# Monday 00:00 UTC = Monday 08:00 Taipei
6+
- cron: "0 0 * * 1"
7+
workflow_dispatch:
8+
9+
jobs:
10+
create-issue:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-python@v5
15+
with:
16+
python-version: "3.12"
17+
- run: pip install requests
18+
- run: python .github/scripts/create_standup_issue.py
19+
env:
20+
GITHUB_REPOSITORY: ${{ github.repository }}
21+
STANDUP_TOKEN: ${{ secrets.STANDUP_TOKEN }}

0 commit comments

Comments
 (0)