Skip to content

Commit 7e9e04d

Browse files
authored
Add PHEP 3 Quarterly Email Reminders Action (#378)
* Add PHEP 3 support schedule page with Gantt chart * Improve Drop and Adoption Schedule formatting * Text updates * Action to update PHEP 3 schedule * Workflow to send quarterly PHEP 3 email reminders * Add test recipient input to email reminder workflow
1 parent 58fe47b commit 7e9e04d

3 files changed

Lines changed: 210 additions & 0 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: PHEP 3 Quarterly Email Reminder
2+
3+
on:
4+
schedule:
5+
# Quarterly: 1st of Jan, Apr, Jul, Oct at 3pm UTC (8am Mountain Time)
6+
- cron: '0 15 1 1,4,7,10 *'
7+
workflow_dispatch: # Allow manual trigger for testing
8+
inputs:
9+
test_recipient:
10+
description: 'Test email recipient (leave empty to send to mailing list)'
11+
required: false
12+
default: ''
13+
14+
jobs:
15+
send-reminder:
16+
runs-on: ubuntu-latest
17+
18+
steps:
19+
- name: Checkout Repository
20+
uses: actions/checkout@v4
21+
22+
- name: Set up Python 3.12
23+
uses: actions/setup-python@v4
24+
with:
25+
python-version: '3.12'
26+
27+
- name: Generate Email Content
28+
run: |
29+
cd _pages/docs/phep-3
30+
python generate_email.py
31+
32+
- name: Read Email Content
33+
id: email
34+
run: |
35+
echo "subject=$(cat _pages/docs/phep-3/email_subject.txt)" >> $GITHUB_OUTPUT
36+
# For multiline body, use delimiter
37+
echo "body<<EOF" >> $GITHUB_OUTPUT
38+
cat _pages/docs/phep-3/email_body.txt >> $GITHUB_OUTPUT
39+
echo "EOF" >> $GITHUB_OUTPUT
40+
41+
- name: Send Email
42+
uses: dawidd6/action-send-mail@v3
43+
with:
44+
server_address: smtp.gmail.com
45+
server_port: 587
46+
username: ${{ secrets.PYHC_EMAIL_ADDRESS }}
47+
password: ${{ secrets.PYHC_EMAIL_APP_PASSWORD }}
48+
subject: ${{ steps.email.outputs.subject }}
49+
body: ${{ steps.email.outputs.body }}
50+
to: ${{ inputs.test_recipient || 'pyhc-list@googlegroups.com' }}
51+
from: PyHC <${{ secrets.PYHC_EMAIL_ADDRESS }}>
52+
content_type: text/plain

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ Gemfile.lock
77
*.DS_Store
88
.history/
99
*CLAUDE.md
10+
11+
# PHEP 3 email reminder temp files
12+
_pages/docs/phep-3/email_subject.txt
13+
_pages/docs/phep-3/email_body.txt
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""
2+
Generate email content for PHEP 3 quarterly reminders.
3+
Parses schedule.md and extracts the current quarter's information.
4+
"""
5+
6+
import re
7+
from datetime import datetime
8+
from pathlib import Path
9+
10+
11+
def get_current_quarter():
12+
"""Return current year and quarter (1-4)."""
13+
now = datetime.now()
14+
quarter = (now.month - 1) // 3 + 1
15+
return now.year, quarter
16+
17+
18+
def parse_schedule(schedule_path):
19+
"""Parse schedule.md and return dict of quarters with their content."""
20+
content = Path(schedule_path).read_text()
21+
22+
# Split by quarter headers (#### YYYY - Quarter N:)
23+
quarter_pattern = r'#### (\d{4}) - Quarter (\d):'
24+
sections = re.split(quarter_pattern, content)
25+
26+
quarters = {}
27+
# sections[0] is empty/before first match, then year, quarter, content triplets
28+
for i in range(1, len(sections) - 2, 3):
29+
year = int(sections[i])
30+
quarter = int(sections[i + 1])
31+
quarter_content = sections[i + 2].strip()
32+
quarters[(year, quarter)] = quarter_content
33+
34+
return quarters
35+
36+
37+
def parse_table(table_text):
38+
"""Parse a markdown table and return list of (package, version, info) tuples."""
39+
lines = table_text.strip().split('\n')
40+
rows = []
41+
for line in lines:
42+
# Skip header and separator rows
43+
if line.startswith('|') and '---' not in line:
44+
cells = [c.strip() for c in line.split('|')[1:-1]]
45+
if len(cells) >= 3 and cells[0]: # Skip empty header rows
46+
rows.append((cells[0], cells[1], cells[2]))
47+
return rows
48+
49+
50+
def extract_quarter_data(quarter_content):
51+
"""Extract adopt and drop tables from quarter content."""
52+
adopt_match = re.search(
53+
r'###### Adopt support for:\s*\n((?:\|.*\n)+)',
54+
quarter_content
55+
)
56+
drop_match = re.search(
57+
r'###### Can drop support for:\s*\n((?:\|.*\n)+)',
58+
quarter_content
59+
)
60+
61+
adopt_items = parse_table(adopt_match.group(1)) if adopt_match else []
62+
drop_items = parse_table(drop_match.group(1)) if drop_match else []
63+
64+
return adopt_items, drop_items
65+
66+
67+
def format_email(year, quarter, adopt_items, drop_items):
68+
"""Generate email subject and body."""
69+
quarter_names = {1: "Q1", 2: "Q2", 3: "Q3", 4: "Q4"}
70+
q_name = quarter_names[quarter]
71+
72+
subject = f"PHEP 3 Reminder: {q_name} {year} Support Schedule"
73+
74+
body = f"""Hello PyHC Community,
75+
76+
This is a quarterly reminder about the PHEP 3 Python & Upstream Package Support Policy.
77+
78+
"""
79+
80+
if adopt_items:
81+
body += f"## Adopt Support For (by end of {q_name} {year})\n\n"
82+
body += "The following package versions should be supported by PyHC packages:\n\n"
83+
for package, version, info in adopt_items:
84+
body += f"- **{package}** {version} ({info})\n"
85+
body += "\n"
86+
87+
if drop_items:
88+
body += f"## Can Drop Support For (as of {q_name} {year})\n\n"
89+
body += "PyHC packages may now drop support for:\n\n"
90+
for package, version, info in drop_items:
91+
body += f"- **{package}** {version} ({info})\n"
92+
body += "\n"
93+
94+
if not adopt_items and not drop_items:
95+
body += "No changes to the support schedule this quarter.\n\n"
96+
97+
body += """---
98+
99+
For the full support schedule and Gantt chart, visit:
100+
https://heliopython.org/docs/pheps/phep-3-support-schedule/
101+
102+
For the complete PHEP 3 specification:
103+
https://github.com/heliophysicsPy/standards/blob/main/pheps/phep-0003.md
104+
105+
Questions? Reply to this email or discuss on PyHC Slack.
106+
107+
Best regards,
108+
PyHC Tech Lead
109+
"""
110+
111+
return subject, body
112+
113+
114+
def main():
115+
script_dir = Path(__file__).parent
116+
schedule_path = script_dir / "schedule.md"
117+
118+
year, quarter = get_current_quarter()
119+
print(f"Current quarter: Q{quarter} {year}")
120+
121+
quarters = parse_schedule(schedule_path)
122+
123+
if (year, quarter) not in quarters:
124+
print(f"Warning: No data found for Q{quarter} {year}")
125+
# Try to find the nearest future quarter
126+
future_quarters = [(y, q) for y, q in quarters.keys() if (y, q) >= (year, quarter)]
127+
if future_quarters:
128+
year, quarter = min(future_quarters)
129+
print(f"Using next available quarter: Q{quarter} {year}")
130+
else:
131+
print("No future quarters found in schedule. Using empty content.")
132+
subject = f"PHEP 3 Reminder: Q{quarter} {year} Support Schedule"
133+
body = "No schedule data available for this quarter. Please check the PHEP 3 support schedule page."
134+
Path(script_dir / "email_subject.txt").write_text(subject)
135+
Path(script_dir / "email_body.txt").write_text(body)
136+
return
137+
138+
quarter_content = quarters[(year, quarter)]
139+
adopt_items, drop_items = extract_quarter_data(quarter_content)
140+
141+
print(f"Found {len(adopt_items)} adopt items, {len(drop_items)} drop items")
142+
143+
subject, body = format_email(year, quarter, adopt_items, drop_items)
144+
145+
# Write outputs for the GitHub Action to use
146+
Path(script_dir / "email_subject.txt").write_text(subject)
147+
Path(script_dir / "email_body.txt").write_text(body)
148+
149+
print(f"\nSubject: {subject}")
150+
print(f"\nBody preview:\n{body[:500]}...")
151+
152+
153+
if __name__ == "__main__":
154+
main()

0 commit comments

Comments
 (0)