Scheduled Extension Checker #103
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: Scheduled Extension Checker | |
| on: | |
| # Run daily at 6 AM UTC | |
| schedule: | |
| - cron: '0 6 * * *' | |
| # Manual trigger for testing | |
| workflow_dispatch: | |
| inputs: | |
| days_ahead: | |
| description: 'Check CFPs closing within N days' | |
| required: false | |
| type: number | |
| default: 3 | |
| dry_run: | |
| description: 'Dry run (show what would be checked without triggering)' | |
| required: false | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: read | |
| jobs: | |
| find-closing-cfps: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| conferences: ${{ steps.find.outputs.conferences }} | |
| count: ${{ steps.find.outputs.count }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Find CFPs closing soon | |
| id: find | |
| env: | |
| # Pass inputs via env to prevent injection | |
| DAYS_AHEAD: ${{ inputs.days_ahead || 3 }} | |
| run: | | |
| TODAY=$(date +%Y-%m-%d) | |
| # Use env vars safely in Python | |
| python3 << 'FINDER' | |
| import yaml | |
| import json | |
| import os | |
| import sys | |
| from datetime import datetime, timedelta | |
| days_ahead = int(os.environ.get('DAYS_AHEAD', '3')) | |
| today = datetime.now().date() | |
| check_until = today + timedelta(days=days_ahead) | |
| try: | |
| with open("_data/conferences.yml") as f: | |
| confs = yaml.safe_load(f) or [] | |
| except Exception as e: | |
| print(f"ERROR: Could not load conferences.yml: {e}", file=sys.stderr) | |
| confs = [] | |
| closing_soon = [] | |
| for conf in confs: | |
| name = conf.get("conference", "Unknown") | |
| # Get the effective CFP deadline | |
| cfp_str = conf.get("cfp_ext") or conf.get("cfp", "TBA") | |
| has_extension = conf.get("cfp_ext") is not None | |
| if cfp_str in ("TBA", "Cancelled", "None"): | |
| continue | |
| try: | |
| cfp_date = datetime.strptime(cfp_str[:10], "%Y-%m-%d").date() | |
| except ValueError: | |
| continue | |
| # Include CFPs closing within window or just closed (extension check) | |
| extension_window_start = today - timedelta(days=3) | |
| if extension_window_start <= cfp_date <= check_until: | |
| url = str(conf.get("link", "")) | |
| if cfp_date < today: | |
| check_type = "extension_check" | |
| reason = f"CFP closed {cfp_date}, checking for extension" | |
| elif cfp_date == today: | |
| check_type = "closing_today" | |
| reason = "CFP closes today!" | |
| else: | |
| days_left = (cfp_date - today).days | |
| check_type = "closing_soon" | |
| reason = f"CFP closes in {days_left} days ({cfp_date})" | |
| # Skip if already has extension and past deadline | |
| if has_extension and cfp_date < today: | |
| print(f"Skip: {name} - already extended, past deadline", file=sys.stderr) | |
| continue | |
| cfp_link = str(conf.get("cfp_link", "")) | |
| closing_soon.append({ | |
| "conference": name, | |
| "url": url, | |
| "cfp_link": cfp_link, | |
| "cfp_date": str(cfp_date), | |
| "has_extension": has_extension, | |
| "check_type": check_type, | |
| "reason": reason | |
| }) | |
| print(f"✓ {name}: {reason}") | |
| print(f"\nFound {len(closing_soon)} conferences to check") | |
| with open("/tmp/closing_cfps.json", "w") as f: | |
| json.dump(closing_soon, f) | |
| FINDER | |
| # Read results safely | |
| CONFERENCES=$(cat /tmp/closing_cfps.json) | |
| COUNT=$(echo "$CONFERENCES" | jq 'length') | |
| echo "conferences=$(echo "$CONFERENCES" | jq -c '.')" >> $GITHUB_OUTPUT | |
| echo "count=$COUNT" >> $GITHUB_OUTPUT | |
| # Summary | |
| echo "## CFPs Closing Soon" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Checking CFPs closing within **$DAYS_AHEAD days**" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ "$COUNT" -gt 0 ]; then | |
| echo "| Conference | CFP Date | Status |" >> $GITHUB_STEP_SUMMARY | |
| echo "|------------|----------|--------|" >> $GITHUB_STEP_SUMMARY | |
| echo "$CONFERENCES" | jq -r '.[] | "| \(.conference) | \(.cfp_date) | \(.reason) |"' >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "No CFPs closing soon ✅" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # Trigger checks for each conference | |
| trigger-checks: | |
| needs: find-closing-cfps | |
| if: needs.find-closing-cfps.outputs.count != '0' && inputs.dry_run != true | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| strategy: | |
| matrix: | |
| conference: ${{ fromJson(needs.find-closing-cfps.outputs.conferences) }} | |
| max-parallel: 1 # Conservative to avoid rate limits | |
| fail-fast: false | |
| steps: | |
| - name: Install lynx | |
| run: sudo apt-get update && sudo apt-get install -y lynx | |
| - name: Fetch website content and trigger check | |
| env: | |
| # Pass matrix values via env for safety | |
| CONF_NAME: ${{ matrix.conference.conference }} | |
| CONF_URL: ${{ matrix.conference.url }} | |
| CONF_CFP_LINK: ${{ matrix.conference.cfp_link }} | |
| CONF_REASON: ${{ matrix.conference.reason }} | |
| CONF_CFP_DATE: ${{ matrix.conference.cfp_date }} | |
| CONF_CHECK_TYPE: ${{ matrix.conference.check_type }} | |
| CONF_HAS_EXT: ${{ matrix.conference.has_extension }} | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| echo "Triggering check for: $CONF_NAME" | |
| echo "Main URL: $CONF_URL" | |
| echo "CFP Link: $CONF_CFP_LINK" | |
| echo "Reason: $CONF_REASON" | |
| echo "CFP Date: $CONF_CFP_DATE" | |
| echo "Check Type: $CONF_CHECK_TYPE" | |
| # Fetch main website content | |
| echo "Fetching main website content..." | |
| MAIN_CONTENT="" | |
| if [ -n "$CONF_URL" ]; then | |
| MAIN_CONTENT=$(lynx -dump -nolist "$CONF_URL" 2>/dev/null | head -500 || echo "Failed to fetch main website") | |
| fi | |
| # Fetch CFP link content | |
| echo "Fetching CFP link content..." | |
| CFP_CONTENT="" | |
| if [ -n "$CONF_CFP_LINK" ] && [ "$CONF_CFP_LINK" != "$CONF_URL" ]; then | |
| CFP_CONTENT=$(lynx -dump -nolist "$CONF_CFP_LINK" 2>/dev/null | head -500 || echo "Failed to fetch CFP page") | |
| fi | |
| # Combine content for analysis (use printf to avoid YAML parsing issues) | |
| COMBINED_CONTENT=$(printf '%s\n\n%s\n%s\n\n%s\n%s' \ | |
| "=== EXTENSION CHECK: $CONF_REASON ===" \ | |
| "=== MAIN WEBSITE ($CONF_URL) ===" \ | |
| "$MAIN_CONTENT" \ | |
| "=== CFP PAGE ($CONF_CFP_LINK) ===" \ | |
| "$CFP_CONTENT") | |
| # Truncate if too long (GitHub has payload limits) | |
| COMBINED_CONTENT=$(echo "$COMBINED_CONTENT" | head -c 60000) | |
| # Use gh CLI for safe API call | |
| # Pass trigger context so triage workflow can adjust prompt accordingly | |
| gh api repos/${{ github.repository }}/dispatches \ | |
| -f event_type=conference-change \ | |
| -f "client_payload[url]=$CONF_URL" \ | |
| -f "client_payload[title]=$CONF_NAME" \ | |
| -f "client_payload[watch_uuid]=" \ | |
| -f "client_payload[diff]=$COMBINED_CONTENT" \ | |
| -f "client_payload[source]=scheduled-checker" \ | |
| -f "client_payload[trigger_reason]=$CONF_CHECK_TYPE" \ | |
| -f "client_payload[original_cfp_deadline]=$CONF_CFP_DATE" \ | |
| -f "client_payload[has_extension]=$CONF_HAS_EXT" | |
| echo "✓ Triggered with website content" | |
| - name: Rate limit pause | |
| run: sleep 10 # Generous pause between triggers | |
| # Summary | |
| summary: | |
| needs: [find-closing-cfps, trigger-checks] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 2 | |
| steps: | |
| - name: Final summary | |
| env: | |
| COUNT: ${{ needs.find-closing-cfps.outputs.count }} | |
| DRY_RUN: ${{ inputs.dry_run }} | |
| run: | | |
| echo "## Extension Checker Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ "$COUNT" = "0" ]; then | |
| echo "✅ No CFPs closing soon" >> $GITHUB_STEP_SUMMARY | |
| elif [ "$DRY_RUN" = "true" ]; then | |
| echo "🔍 **Dry run** - found $COUNT conferences (not triggered)" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "🚀 Triggered checks for **$COUNT** conferences" >> $GITHUB_STEP_SUMMARY | |
| fi |