Security Working Group Meeting Agenda #38
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Security Working Group Meeting Agenda | |
| on: | |
| schedule: | |
| # Run every Thursday at 4 PM UTC (9 AM PT / 8 AM PST) | |
| - cron: "0 16 * * 4" | |
| workflow_dispatch: | |
| inputs: | |
| meeting_date: | |
| description: "Meeting date override (YYYY-MM-DD) - defaults to next Monday" | |
| required: false | |
| type: string | |
| force_create: | |
| description: "Force create even if issue already exists" | |
| required: false | |
| default: false | |
| type: boolean | |
| env: | |
| REPO_OWNER: "openjs-foundation" | |
| REPO_NAME: "security-wg" | |
| AGENDA_LABEL: "security-meeting-agenda-item" | |
| ISSUE_LABEL: "security-meeting-agenda" | |
| # HackMD team path - the part after hackmd.io/@ in your team URL | |
| # Leave empty to create notes in personal workspace | |
| HACKMD_TEAM: "openjs-security" | |
| # iCal feed for the OpenJS Foundation calendar | |
| ICAL_FEED_URL: "https://webcal.prod.itx.linuxfoundation.org/lfx/a0941000002wBygAAE" | |
| MEETING_TITLE_MATCH: "Security Working Group" | |
| jobs: | |
| create-meeting-agenda: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| issues: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Install Python iCal dependencies | |
| run: pip install icalendar python-dateutil | |
| - name: Check calendar for upcoming meeting | |
| id: calendar-check | |
| run: | | |
| if [ -n "${{ github.event.inputs.meeting_date }}" ]; then | |
| TARGET_DATE="${{ github.event.inputs.meeting_date }}" | |
| else | |
| TARGET_DATE=$(date -d "next monday" +%Y-%m-%d) | |
| fi | |
| echo "Checking calendar for meeting on $TARGET_DATE..." | |
| # Fetch the iCal feed | |
| curl -sf -o /tmp/calendar.ics "${{ env.ICAL_FEED_URL }}" | |
| # Parse the iCal feed and check for a matching meeting | |
| python3 << 'PYEOF' | |
| import sys | |
| import os | |
| from datetime import datetime, timedelta | |
| from icalendar import Calendar | |
| from dateutil.rrule import rrulestr | |
| from dateutil import tz | |
| target_str = os.environ["TARGET_DATE"] | |
| match_title = os.environ["MEETING_TITLE_MATCH"] | |
| target = datetime.strptime(target_str, "%Y-%m-%d").date() | |
| with open("/tmp/calendar.ics", "rb") as f: | |
| cal = Calendar.from_ical(f.read()) | |
| found = False | |
| for component in cal.walk(): | |
| if component.name != "VEVENT": | |
| continue | |
| summary = str(component.get("SUMMARY", "")) | |
| if match_title.lower() not in summary.lower(): | |
| continue | |
| dtstart = component.get("DTSTART").dt | |
| # Handle timezone-aware datetimes | |
| if hasattr(dtstart, "date"): | |
| start_date = dtstart.date() | |
| else: | |
| start_date = dtstart | |
| rrule = component.get("RRULE") | |
| if rrule: | |
| # Expand recurrence rule to check if target date is an occurrence | |
| rule_str = rrule.to_ical().decode() | |
| rule = rrulestr(rule_str, dtstart=dtstart) | |
| # Check a window around the target date | |
| window_start = datetime.combine(target, datetime.min.time()) | |
| window_end = datetime.combine(target, datetime.max.time()) | |
| if hasattr(dtstart, 'tzinfo') and dtstart.tzinfo: | |
| window_start = window_start.replace(tzinfo=dtstart.tzinfo) | |
| window_end = window_end.replace(tzinfo=dtstart.tzinfo) | |
| occurrences = rule.between(window_start, window_end, inc=True) | |
| if occurrences: | |
| found = True | |
| print(f"Found meeting: '{summary}' on {target}") | |
| break | |
| else: | |
| # Single event, check if it falls on the target date | |
| if start_date == target: | |
| found = True | |
| print(f"Found meeting: '{summary}' on {target}") | |
| break | |
| if found: | |
| print("MEETING_FOUND=true") | |
| else: | |
| print(f"No '{match_title}' meeting found on {target}") | |
| print("MEETING_FOUND=false") | |
| # Write output for GitHub Actions | |
| with open(os.environ["GITHUB_OUTPUT"], "a") as f: | |
| f.write(f"meeting_found={'true' if found else 'false'}\n") | |
| f.write(f"target_date={target_str}\n") | |
| PYEOF | |
| echo "target_date=$TARGET_DATE" >> $GITHUB_OUTPUT | |
| env: | |
| TARGET_DATE: ${{ github.event.inputs.meeting_date || '' }} | |
| MEETING_TITLE_MATCH: ${{ env.MEETING_TITLE_MATCH }} | |
| - name: Skip - no meeting on calendar | |
| if: steps.calendar-check.outputs.meeting_found != 'true' && github.event.inputs.force_create != 'true' | |
| run: | | |
| echo "## No Meeting Found" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "No '${{ env.MEETING_TITLE_MATCH }}' meeting found on the calendar for ${{ steps.calendar-check.outputs.target_date }}." >> $GITHUB_STEP_SUMMARY | |
| echo "Skipping agenda creation." >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "*To force creation, re-run with force_create enabled.*" >> $GITHUB_STEP_SUMMARY | |
| - name: Get meeting date info | |
| id: meeting-info | |
| if: steps.calendar-check.outputs.meeting_found == 'true' || github.event.inputs.force_create == 'true' | |
| run: | | |
| if [ -n "${{ github.event.inputs.meeting_date }}" ]; then | |
| MEETING_DATE_INPUT="${{ github.event.inputs.meeting_date }}" | |
| MEETING_DATE=$(date -d "$MEETING_DATE_INPUT" +"%B %d, %Y") | |
| MEETING_DATE_SHORT="$MEETING_DATE_INPUT" | |
| else | |
| MEETING_DATE_SHORT=$(date -d "next monday" +%Y-%m-%d) | |
| MEETING_DATE=$(date -d "next monday" +"%B %d, %Y") | |
| fi | |
| # Find the next meeting date after this one by checking the calendar | |
| python3 << PYEOF | |
| import os | |
| from datetime import datetime, timedelta | |
| from icalendar import Calendar | |
| from dateutil.rrule import rrulestr | |
| target_str = "$MEETING_DATE_SHORT" | |
| match_title = os.environ["MEETING_TITLE_MATCH"] | |
| target = datetime.strptime(target_str, "%Y-%m-%d").date() | |
| with open("/tmp/calendar.ics", "rb") as f: | |
| cal = Calendar.from_ical(f.read()) | |
| # Find the next occurrence after target_date | |
| next_date = None | |
| for component in cal.walk(): | |
| if component.name != "VEVENT": | |
| continue | |
| summary = str(component.get("SUMMARY", "")) | |
| if match_title.lower() not in summary.lower(): | |
| continue | |
| dtstart = component.get("DTSTART").dt | |
| rrule = component.get("RRULE") | |
| if rrule: | |
| rule_str = rrule.to_ical().decode() | |
| rule = rrulestr(rule_str, dtstart=dtstart) | |
| # Look for the first occurrence after target | |
| search_start = datetime.combine(target + timedelta(days=1), datetime.min.time()) | |
| if hasattr(dtstart, 'tzinfo') and dtstart.tzinfo: | |
| search_start = search_start.replace(tzinfo=dtstart.tzinfo) | |
| future = rule.after(search_start) | |
| if future: | |
| candidate = future.date() if hasattr(future, 'date') else future | |
| if next_date is None or candidate < next_date: | |
| next_date = candidate | |
| if next_date: | |
| formatted = next_date.strftime("%B %d, %Y") | |
| else: | |
| # Fallback to 2 weeks from now | |
| formatted = (target + timedelta(days=14)).strftime("%B %d, %Y") | |
| with open(os.environ["GITHUB_OUTPUT"], "a") as f: | |
| f.write(f"next_meeting_date={formatted}\n") | |
| PYEOF | |
| echo "meeting_date=$MEETING_DATE" >> $GITHUB_OUTPUT | |
| echo "meeting_date_short=$MEETING_DATE_SHORT" >> $GITHUB_OUTPUT | |
| # Create issue title | |
| ISSUE_TITLE="Security Working Group Meeting - $MEETING_DATE" | |
| echo "issue_title=$ISSUE_TITLE" >> $GITHUB_OUTPUT | |
| env: | |
| MEETING_TITLE_MATCH: ${{ env.MEETING_TITLE_MATCH }} | |
| - name: Check for existing issue | |
| id: check-issue | |
| if: steps.calendar-check.outputs.meeting_found == 'true' || github.event.inputs.force_create == 'true' | |
| run: | | |
| EXISTING_ISSUE=$(gh issue list \ | |
| --repo "${{ env.REPO_OWNER }}/${{ env.REPO_NAME }}" \ | |
| --label "${{ env.ISSUE_LABEL }}" \ | |
| --search "${{ steps.meeting-info.outputs.meeting_date_short }} in:title" \ | |
| --json number,url \ | |
| --jq '.[0]') | |
| if [ -n "$EXISTING_ISSUE" ] && [ "$EXISTING_ISSUE" != "null" ]; then | |
| echo "existing_issue=true" >> $GITHUB_OUTPUT | |
| echo "issue_number=$(echo $EXISTING_ISSUE | jq -r '.number')" >> $GITHUB_OUTPUT | |
| echo "issue_url=$(echo $EXISTING_ISSUE | jq -r '.url')" >> $GITHUB_OUTPUT | |
| else | |
| echo "existing_issue=false" >> $GITHUB_OUTPUT | |
| fi | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Fetch issues with security-agenda label | |
| id: fetch-agenda-issues | |
| if: steps.calendar-check.outputs.meeting_found == 'true' || github.event.inputs.force_create == 'true' | |
| run: | | |
| # Fetch all open issues with the security-agenda label | |
| AGENDA_ISSUES=$(gh issue list \ | |
| --repo "${{ env.REPO_OWNER }}/${{ env.REPO_NAME }}" \ | |
| --label "${{ env.AGENDA_LABEL }}" \ | |
| --state open \ | |
| --json number,title,url \ | |
| --jq '.[] | "- [ ] [#\(.number)](\(.url)) - \(.title)"') | |
| if [ -z "$AGENDA_ISSUES" ]; then | |
| AGENDA_ISSUES="*No issues currently labeled with \`${{ env.AGENDA_LABEL }}\`*" | |
| fi | |
| # Count the issues | |
| ISSUE_COUNT=$(gh issue list \ | |
| --repo "${{ env.REPO_OWNER }}/${{ env.REPO_NAME }}" \ | |
| --label "${{ env.AGENDA_LABEL }}" \ | |
| --state open \ | |
| --json number \ | |
| --jq 'length') | |
| # Save to file to handle multi-line content | |
| echo "$AGENDA_ISSUES" > /tmp/agenda_issues.md | |
| echo "issue_count=$ISSUE_COUNT" >> $GITHUB_OUTPUT | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Load and prepare HackMD template | |
| id: prepare-hackmd | |
| if: steps.calendar-check.outputs.meeting_found == 'true' || github.event.inputs.force_create == 'true' | |
| run: | | |
| # Read the template | |
| TEMPLATE_CONTENT=$(cat .github/templates/hackmd-agenda-template.md) | |
| # Read agenda issues | |
| AGENDA_ITEMS=$(cat /tmp/agenda_issues.md) | |
| # Replace placeholders | |
| TEMPLATE_CONTENT="${TEMPLATE_CONTENT//\[DATE\]/${{ steps.meeting-info.outputs.meeting_date }}}" | |
| TEMPLATE_CONTENT="${TEMPLATE_CONTENT//\[NEXT_MEETING_DATE\]/${{ steps.meeting-info.outputs.next_meeting_date }}}" | |
| TEMPLATE_CONTENT="${TEMPLATE_CONTENT//\[AGENDA_ITEMS\]/$AGENDA_ITEMS}" | |
| # Save to file for API call | |
| echo "$TEMPLATE_CONTENT" > /tmp/hackmd_content.md | |
| - name: Create HackMD document | |
| id: create-hackmd | |
| if: (steps.calendar-check.outputs.meeting_found == 'true' || github.event.inputs.force_create == 'true') && (steps.check-issue.outputs.existing_issue == 'false' || github.event.inputs.force_create == 'true') | |
| run: | | |
| # Read prepared content | |
| HACKMD_CONTENT=$(cat /tmp/hackmd_content.md) | |
| # Determine API endpoint based on team setting | |
| if [ -n "${{ env.HACKMD_TEAM }}" ]; then | |
| HACKMD_API_URL="https://api.hackmd.io/v1/teams/${{ env.HACKMD_TEAM }}/notes" | |
| else | |
| HACKMD_API_URL="https://api.hackmd.io/v1/notes" | |
| fi | |
| echo "Creating HackMD document at: $HACKMD_API_URL" | |
| # Create HackMD document via API | |
| HACKMD_RESPONSE=$(curl -s -X POST \ | |
| -H "Authorization: Bearer ${{ secrets.HACKMD_API_TOKEN }}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{ | |
| \"title\": \"Security Working Group Meeting - ${{ steps.meeting-info.outputs.meeting_date }}\", | |
| \"content\": $(echo "$HACKMD_CONTENT" | jq -Rs .), | |
| \"readPermission\": \"guest\", | |
| \"writePermission\": \"signed_in\", | |
| \"commentPermission\": \"everyone\" | |
| }" \ | |
| "$HACKMD_API_URL") | |
| # Extract the publish link | |
| HACKMD_ID=$(echo "$HACKMD_RESPONSE" | jq -r '.id // empty') | |
| HACKMD_PUBLISH_LINK=$(echo "$HACKMD_RESPONSE" | jq -r '.publishLink // empty') | |
| if [ -z "$HACKMD_ID" ]; then | |
| echo "Error creating HackMD document:" | |
| echo "$HACKMD_RESPONSE" | |
| exit 1 | |
| fi | |
| # If no publish link, construct it with team path if available | |
| if [ -z "$HACKMD_PUBLISH_LINK" ] || [ "$HACKMD_PUBLISH_LINK" == "null" ]; then | |
| if [ -n "${{ env.HACKMD_TEAM }}" ]; then | |
| HACKMD_PUBLISH_LINK="https://hackmd.io/@${{ env.HACKMD_TEAM }}/$HACKMD_ID" | |
| else | |
| HACKMD_PUBLISH_LINK="https://hackmd.io/$HACKMD_ID" | |
| fi | |
| fi | |
| echo "hackmd_url=$HACKMD_PUBLISH_LINK" >> $GITHUB_OUTPUT | |
| echo "hackmd_id=$HACKMD_ID" >> $GITHUB_OUTPUT | |
| echo "HackMD document created: $HACKMD_PUBLISH_LINK" | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Update HackMD content with issue URL | |
| if: steps.create-hackmd.outputs.hackmd_id != '' | |
| run: | | |
| # This step will update the HackMD doc after the issue is created | |
| # to include the issue URL (handled in next step) | |
| echo "HackMD document ready for issue URL update" | |
| - name: Create GitHub Issue | |
| id: create-issue | |
| if: (steps.calendar-check.outputs.meeting_found == 'true' || github.event.inputs.force_create == 'true') && (steps.check-issue.outputs.existing_issue == 'false' || github.event.inputs.force_create == 'true') | |
| run: | | |
| HACKMD_URL="${{ steps.create-hackmd.outputs.hackmd_url }}" | |
| # Read agenda items for the issue body | |
| AGENDA_ITEMS=$(cat /tmp/agenda_issues.md) | |
| # Create issue body following the CPC format | |
| ISSUE_BODY=$(cat << 'EOF' | |
| # Security Working Group Meeting - ${{ steps.meeting-info.outputs.meeting_date }} | |
| ## Meeting Details | |
| - **Date**: ${{ steps.meeting-info.outputs.meeting_date }} | |
| - **Time**: 08:30 – 09:30 AM PT | |
| - **Zoom**: [Zoom Link]( https://zoom-lfx.platform.linuxfoundation.org/meeting/98301969246?password=889b7578-29b6-4be9-96c1-f74cbce812c4) | |
| - **Calendar**: [OpenJS Calendar](https://calendar.openjsf.org) | |
| --- | |
| ## Meeting Agenda Items | |
| The following issues are labeled with \`security-agenda\` and will be discussed at this meeting: | |
| EOF | |
| ) | |
| # Append agenda items | |
| ISSUE_BODY="$ISSUE_BODY | |
| $AGENDA_ITEMS | |
| --- | |
| ## Meeting Notes | |
| **HackMD**: $HACKMD_URL | |
| --- | |
| *This agenda was automatically generated. To add items to a future meeting agenda, apply the \`${{ env.AGENDA_LABEL }}\` label to the relevant issue.* | |
| *Issues labeled: ${{ steps.fetch-agenda-issues.outputs.issue_count }}*" | |
| # Create the issue | |
| ISSUE_RESPONSE=$(gh issue create \ | |
| --repo "${{ env.REPO_OWNER }}/${{ env.REPO_NAME }}" \ | |
| --title "${{ steps.meeting-info.outputs.issue_title }}" \ | |
| --body "$ISSUE_BODY" \ | |
| --label "${{ env.ISSUE_LABEL }}") | |
| echo "issue_url=$ISSUE_RESPONSE" >> $GITHUB_OUTPUT | |
| echo "Issue created: $ISSUE_RESPONSE" | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Update HackMD with issue URL | |
| if: steps.create-issue.outputs.issue_url != '' && steps.create-hackmd.outputs.hackmd_id != '' | |
| run: | | |
| # Re-read the template and update with issue URL | |
| TEMPLATE_CONTENT=$(cat .github/templates/hackmd-agenda-template.md) | |
| AGENDA_ITEMS=$(cat /tmp/agenda_issues.md) | |
| # Replace all placeholders including the issue URL | |
| TEMPLATE_CONTENT="${TEMPLATE_CONTENT//\[DATE\]/${{ steps.meeting-info.outputs.meeting_date }}}" | |
| TEMPLATE_CONTENT="${TEMPLATE_CONTENT//\[NEXT_MEETING_DATE\]/${{ steps.meeting-info.outputs.next_meeting_date }}}" | |
| TEMPLATE_CONTENT="${TEMPLATE_CONTENT//\[AGENDA_ITEMS\]/$AGENDA_ITEMS}" | |
| TEMPLATE_CONTENT="${TEMPLATE_CONTENT//\[ISSUE_URL\]/${{ steps.create-issue.outputs.issue_url }}}" | |
| # Update HackMD document | |
| curl -s -X PATCH \ | |
| -H "Authorization: Bearer ${{ secrets.HACKMD_API_TOKEN }}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{ | |
| \"content\": $(echo "$TEMPLATE_CONTENT" | jq -Rs .) | |
| }" \ | |
| "https://api.hackmd.io/v1/notes/${{ steps.create-hackmd.outputs.hackmd_id }}" | |
| echo "HackMD document updated with issue URL" | |
| - name: Summary | |
| if: steps.calendar-check.outputs.meeting_found == 'true' || github.event.inputs.force_create == 'true' | |
| run: | | |
| echo "## Meeting Agenda Creation Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ steps.check-issue.outputs.existing_issue }}" == "true" ] && [ "${{ github.event.inputs.force_create }}" != "true" ]; then | |
| echo "### Skipped - Issue Already Exists" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Existing Issue**: ${{ steps.check-issue.outputs.issue_url }}" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "### Created Successfully" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Meeting Date**: ${{ steps.meeting-info.outputs.meeting_date }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **GitHub Issue**: ${{ steps.create-issue.outputs.issue_url }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **HackMD Document**: ${{ steps.create-hackmd.outputs.hackmd_url }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Agenda Items Found**: ${{ steps.fetch-agenda-issues.outputs.issue_count }}" >> $GITHUB_STEP_SUMMARY | |
| fi |