diff --git a/.github/workflows/phep3_email_reminder.yml b/.github/workflows/phep3_email_reminder.yml new file mode 100644 index 0000000..0dc8e2c --- /dev/null +++ b/.github/workflows/phep3_email_reminder.yml @@ -0,0 +1,52 @@ +name: PHEP 3 Quarterly Email Reminder + +on: + schedule: + # Quarterly: 1st of Jan, Apr, Jul, Oct at 3pm UTC (8am Mountain Time) + - cron: '0 15 1 1,4,7,10 *' + workflow_dispatch: # Allow manual trigger for testing + inputs: + test_recipient: + description: 'Test email recipient (leave empty to send to mailing list)' + required: false + default: '' + +jobs: + send-reminder: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Generate Email Content + run: | + cd _pages/docs/phep-3 + python generate_email.py + + - name: Read Email Content + id: email + run: | + echo "subject=$(cat _pages/docs/phep-3/email_subject.txt)" >> $GITHUB_OUTPUT + # For multiline body, use delimiter + echo "body<> $GITHUB_OUTPUT + cat _pages/docs/phep-3/email_body.txt >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Send Email + uses: dawidd6/action-send-mail@v3 + with: + server_address: smtp.gmail.com + server_port: 587 + username: ${{ secrets.PYHC_EMAIL_ADDRESS }} + password: ${{ secrets.PYHC_EMAIL_APP_PASSWORD }} + subject: ${{ steps.email.outputs.subject }} + body: ${{ steps.email.outputs.body }} + to: ${{ inputs.test_recipient || 'pyhc-list@googlegroups.com' }} + from: PyHC <${{ secrets.PYHC_EMAIL_ADDRESS }}> + content_type: text/plain diff --git a/.gitignore b/.gitignore index 05e704d..8d104d8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ Gemfile.lock *.DS_Store .history/ *CLAUDE.md + +# PHEP 3 email reminder temp files +_pages/docs/phep-3/email_subject.txt +_pages/docs/phep-3/email_body.txt diff --git a/_pages/docs/phep-3/generate_email.py b/_pages/docs/phep-3/generate_email.py new file mode 100644 index 0000000..38d7ed1 --- /dev/null +++ b/_pages/docs/phep-3/generate_email.py @@ -0,0 +1,154 @@ +""" +Generate email content for PHEP 3 quarterly reminders. +Parses schedule.md and extracts the current quarter's information. +""" + +import re +from datetime import datetime +from pathlib import Path + + +def get_current_quarter(): + """Return current year and quarter (1-4).""" + now = datetime.now() + quarter = (now.month - 1) // 3 + 1 + return now.year, quarter + + +def parse_schedule(schedule_path): + """Parse schedule.md and return dict of quarters with their content.""" + content = Path(schedule_path).read_text() + + # Split by quarter headers (#### YYYY - Quarter N:) + quarter_pattern = r'#### (\d{4}) - Quarter (\d):' + sections = re.split(quarter_pattern, content) + + quarters = {} + # sections[0] is empty/before first match, then year, quarter, content triplets + for i in range(1, len(sections) - 2, 3): + year = int(sections[i]) + quarter = int(sections[i + 1]) + quarter_content = sections[i + 2].strip() + quarters[(year, quarter)] = quarter_content + + return quarters + + +def parse_table(table_text): + """Parse a markdown table and return list of (package, version, info) tuples.""" + lines = table_text.strip().split('\n') + rows = [] + for line in lines: + # Skip header and separator rows + if line.startswith('|') and '---' not in line: + cells = [c.strip() for c in line.split('|')[1:-1]] + if len(cells) >= 3 and cells[0]: # Skip empty header rows + rows.append((cells[0], cells[1], cells[2])) + return rows + + +def extract_quarter_data(quarter_content): + """Extract adopt and drop tables from quarter content.""" + adopt_match = re.search( + r'###### Adopt support for:\s*\n((?:\|.*\n)+)', + quarter_content + ) + drop_match = re.search( + r'###### Can drop support for:\s*\n((?:\|.*\n)+)', + quarter_content + ) + + adopt_items = parse_table(adopt_match.group(1)) if adopt_match else [] + drop_items = parse_table(drop_match.group(1)) if drop_match else [] + + return adopt_items, drop_items + + +def format_email(year, quarter, adopt_items, drop_items): + """Generate email subject and body.""" + quarter_names = {1: "Q1", 2: "Q2", 3: "Q3", 4: "Q4"} + q_name = quarter_names[quarter] + + subject = f"PHEP 3 Reminder: {q_name} {year} Support Schedule" + + body = f"""Hello PyHC Community, + +This is a quarterly reminder about the PHEP 3 Python & Upstream Package Support Policy. + +""" + + if adopt_items: + body += f"## Adopt Support For (by end of {q_name} {year})\n\n" + body += "The following package versions should be supported by PyHC packages:\n\n" + for package, version, info in adopt_items: + body += f"- **{package}** {version} ({info})\n" + body += "\n" + + if drop_items: + body += f"## Can Drop Support For (as of {q_name} {year})\n\n" + body += "PyHC packages may now drop support for:\n\n" + for package, version, info in drop_items: + body += f"- **{package}** {version} ({info})\n" + body += "\n" + + if not adopt_items and not drop_items: + body += "No changes to the support schedule this quarter.\n\n" + + body += """--- + +For the full support schedule and Gantt chart, visit: +https://heliopython.org/docs/pheps/phep-3-support-schedule/ + +For the complete PHEP 3 specification: +https://github.com/heliophysicsPy/standards/blob/main/pheps/phep-0003.md + +Questions? Reply to this email or discuss on PyHC Slack. + +Best regards, +PyHC Tech Lead +""" + + return subject, body + + +def main(): + script_dir = Path(__file__).parent + schedule_path = script_dir / "schedule.md" + + year, quarter = get_current_quarter() + print(f"Current quarter: Q{quarter} {year}") + + quarters = parse_schedule(schedule_path) + + if (year, quarter) not in quarters: + print(f"Warning: No data found for Q{quarter} {year}") + # Try to find the nearest future quarter + future_quarters = [(y, q) for y, q in quarters.keys() if (y, q) >= (year, quarter)] + if future_quarters: + year, quarter = min(future_quarters) + print(f"Using next available quarter: Q{quarter} {year}") + else: + print("No future quarters found in schedule. Using empty content.") + subject = f"PHEP 3 Reminder: Q{quarter} {year} Support Schedule" + body = "No schedule data available for this quarter. Please check the PHEP 3 support schedule page." + Path(script_dir / "email_subject.txt").write_text(subject) + Path(script_dir / "email_body.txt").write_text(body) + return + + quarter_content = quarters[(year, quarter)] + adopt_items, drop_items = extract_quarter_data(quarter_content) + + print(f"Found {len(adopt_items)} adopt items, {len(drop_items)} drop items") + + subject, body = format_email(year, quarter, adopt_items, drop_items) + + # Write outputs for the GitHub Action to use + Path(script_dir / "email_subject.txt").write_text(subject) + Path(script_dir / "email_body.txt").write_text(body) + + print(f"\nSubject: {subject}") + print(f"\nBody preview:\n{body[:500]}...") + + +if __name__ == "__main__": + main()