Skip to content

Security Working Group Meeting Agenda #17

Security Working Group Meeting Agenda

Security Working Group Meeting Agenda #17

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-agenda"
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