From 8cf7d85a3b98cd9929323220a1bb49bc8c87d3ed Mon Sep 17 00:00:00 2001 From: jongfeel Date: Mon, 1 Jun 2026 18:54:18 +0900 Subject: [PATCH 1/3] Add GitHub Action to register meetup events to Google Calendar Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/add-to-google-calendar.yml | 76 ++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 .github/workflows/add-to-google-calendar.yml diff --git a/.github/workflows/add-to-google-calendar.yml b/.github/workflows/add-to-google-calendar.yml new file mode 100644 index 0000000..4d0bd4c --- /dev/null +++ b/.github/workflows/add-to-google-calendar.yml @@ -0,0 +1,76 @@ +name: Add Meetup to Google Calendar + +on: + issues: + types: [opened] + +jobs: + add-to-calendar: + runs-on: ubuntu-latest + # 모각코 이슈 형식에만 실행 (예: "285th online meetup, 2026-05-02") + if: ${{ contains(github.event.issue.title, 'online meetup,') }} + + steps: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install dependencies + run: pip install google-auth google-api-python-client pytz + + - name: Add event to Google Calendar + env: + GOOGLE_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }} + GOOGLE_CALENDAR_ID: ${{ secrets.GOOGLE_CALENDAR_ID }} + ISSUE_TITLE: ${{ github.event.issue.title }} + ISSUE_URL: ${{ github.event.issue.html_url }} + run: | + python - <<'PYTHON' + import os + import json + import re + from datetime import datetime + import pytz + from google.oauth2 import service_account + from googleapiclient.discovery import build + + title = os.environ['ISSUE_TITLE'] + match = re.match(r'(\d+)th online meetup, (\d{4}-\d{2}-\d{2})', title) + if not match: + print(f"Skipping: title doesn't match meetup format: {title}") + exit(0) + + meeting_num = match.group(1) + date_str = match.group(2) + + tz = pytz.timezone('Asia/Seoul') + date = datetime.strptime(date_str, '%Y-%m-%d') + start = tz.localize(date.replace(hour=10, minute=30)) + end = tz.localize(date.replace(hour=12, minute=30)) + + key_info = json.loads(os.environ['GOOGLE_SERVICE_ACCOUNT_KEY']) + credentials = service_account.Credentials.from_service_account_info( + key_info, + scopes=['https://www.googleapis.com/auth/calendar'] + ) + service = build('calendar', 'v3', credentials=credentials) + + event = { + 'summary': f'온라인 모각코 {meeting_num}회', + 'location': 'https://meet.google.com/jyx-mxnq-kpk', + 'description': ( + f'Google Meet: https://meet.google.com/jyx-mxnq-kpk\n\n' + f'Issue: {os.environ["ISSUE_URL"]}' + ), + 'start': {'dateTime': start.isoformat(), 'timeZone': 'Asia/Seoul'}, + 'end': {'dateTime': end.isoformat(), 'timeZone': 'Asia/Seoul'}, + } + + result = service.events().insert( + calendarId=os.environ['GOOGLE_CALENDAR_ID'], + body=event + ).execute() + + print(f"Event created: {result.get('htmlLink')}") + PYTHON From 197010cdf8dc817b8ba9ae5119cffa379b8f4df4 Mon Sep 17 00:00:00 2001 From: jongfeel Date: Mon, 1 Jun 2026 19:18:44 +0900 Subject: [PATCH 2/3] Add assignee as Google Calendar guest on issue assignment Co-Authored-By: Claude Sonnet 4.6 --- .../workflows/add-assignee-to-calendar.yml | 89 +++++++++++++++++++ .github/workflows/add-to-google-calendar.yml | 21 +++-- 2 files changed, 101 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/add-assignee-to-calendar.yml diff --git a/.github/workflows/add-assignee-to-calendar.yml b/.github/workflows/add-assignee-to-calendar.yml new file mode 100644 index 0000000..fe765a7 --- /dev/null +++ b/.github/workflows/add-assignee-to-calendar.yml @@ -0,0 +1,89 @@ +name: Add Assignee to Google Calendar Event + +on: + issues: + types: [assigned] + +jobs: + add-assignee: + runs-on: ubuntu-latest + # 모각코 이슈 형식에만 실행 (예: "285th online meetup, 2026-05-02") + if: ${{ contains(github.event.issue.title, 'online meetup,') }} + + steps: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install dependencies + run: pip install google-auth google-api-python-client + + - name: Add assignee as guest to calendar event + env: + GOOGLE_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }} + GOOGLE_CALENDAR_ID: ${{ secrets.GOOGLE_CALENDAR_ID }} + ASSIGNEE_LOGIN: ${{ github.event.assignee.login }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + python - <<'PYTHON' + import os, json, time, subprocess + from google.oauth2 import service_account + from googleapiclient.discovery import build + + # GitHub 계정의 public email 조회 + login = os.environ['ASSIGNEE_LOGIN'] + result = subprocess.run( + ['gh', 'api', f'/users/{login}', '--jq', '.email'], + capture_output=True, text=True, env={**os.environ} + ) + email = result.stdout.strip() + if not email or email == 'null': + print(f"Skipping {login}: public email not set on GitHub account") + exit(0) + + key_info = json.loads(os.environ['GOOGLE_SERVICE_ACCOUNT_KEY']) + credentials = service_account.Credentials.from_service_account_info( + key_info, scopes=['https://www.googleapis.com/auth/calendar'] + ) + service = build('calendar', 'v3', credentials=credentials) + calendar_id = os.environ['GOOGLE_CALENDAR_ID'] + issue_number = os.environ['ISSUE_NUMBER'] + + # issues:opened 워크플로우와의 경쟁 조건 처리: + # 이슈 생성 시 opened와 assigned가 동시에 트리거되므로 + # 캘린더 이벤트가 아직 생성 중일 수 있어 최대 3회 재시도 + event = None + for attempt in range(3): + events = service.events().list( + calendarId=calendar_id, + privateExtendedProperty=f'github_issue_number={issue_number}' + ).execute() + items = events.get('items', []) + if items: + event = items[0] + break + print(f"Event not found yet, retrying in 15s... ({attempt + 1}/3)") + time.sleep(15) + + if not event: + print(f"Calendar event not found for issue #{issue_number}") + exit(1) + + # 중복 추가 방지 + attendees = event.get('attendees', []) + if any(a.get('email') == email for a in attendees): + print(f"{login} ({email}) is already a guest") + exit(0) + + attendees.append({'email': email}) + service.events().patch( + calendarId=calendar_id, + eventId=event['id'], + body={'attendees': attendees}, + sendUpdates='none', + ).execute() + + print(f"Added {login} ({email}) as guest to the calendar event") + PYTHON diff --git a/.github/workflows/add-to-google-calendar.yml b/.github/workflows/add-to-google-calendar.yml index 4d0bd4c..97ae4c8 100644 --- a/.github/workflows/add-to-google-calendar.yml +++ b/.github/workflows/add-to-google-calendar.yml @@ -25,11 +25,10 @@ jobs: GOOGLE_CALENDAR_ID: ${{ secrets.GOOGLE_CALENDAR_ID }} ISSUE_TITLE: ${{ github.event.issue.title }} ISSUE_URL: ${{ github.event.issue.html_url }} + ISSUE_NUMBER: ${{ github.event.issue.number }} run: | python - <<'PYTHON' - import os - import json - import re + import os, json, re from datetime import datetime import pytz from google.oauth2 import service_account @@ -41,9 +40,7 @@ jobs: print(f"Skipping: title doesn't match meetup format: {title}") exit(0) - meeting_num = match.group(1) - date_str = match.group(2) - + meeting_num, date_str = match.group(1), match.group(2) tz = pytz.timezone('Asia/Seoul') date = datetime.strptime(date_str, '%Y-%m-%d') start = tz.localize(date.replace(hour=10, minute=30)) @@ -51,8 +48,7 @@ jobs: key_info = json.loads(os.environ['GOOGLE_SERVICE_ACCOUNT_KEY']) credentials = service_account.Credentials.from_service_account_info( - key_info, - scopes=['https://www.googleapis.com/auth/calendar'] + key_info, scopes=['https://www.googleapis.com/auth/calendar'] ) service = build('calendar', 'v3', credentials=credentials) @@ -65,11 +61,18 @@ jobs: ), 'start': {'dateTime': start.isoformat(), 'timeZone': 'Asia/Seoul'}, 'end': {'dateTime': end.isoformat(), 'timeZone': 'Asia/Seoul'}, + # issues: assigned 워크플로우에서 이 이벤트를 찾기 위해 이슈 번호를 저장 + 'extendedProperties': { + 'private': { + 'github_issue_number': os.environ['ISSUE_NUMBER'] + } + }, } result = service.events().insert( calendarId=os.environ['GOOGLE_CALENDAR_ID'], - body=event + body=event, + sendUpdates='none', ).execute() print(f"Event created: {result.get('htmlLink')}") From 8cf96c0b84964a3659f9dd2c5bedc5c52c24cffe Mon Sep 17 00:00:00 2001 From: jongfeel Date: Mon, 1 Jun 2026 19:24:41 +0900 Subject: [PATCH 3/3] Fallback to CALENDAR_EMAIL_MAP variable when GitHub public email is missing Co-Authored-By: Claude Sonnet 4.6 --- .../workflows/add-assignee-to-calendar.yml | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/add-assignee-to-calendar.yml b/.github/workflows/add-assignee-to-calendar.yml index fe765a7..ec078cb 100644 --- a/.github/workflows/add-assignee-to-calendar.yml +++ b/.github/workflows/add-assignee-to-calendar.yml @@ -26,22 +26,34 @@ jobs: ASSIGNEE_LOGIN: ${{ github.event.assignee.login }} ISSUE_NUMBER: ${{ github.event.issue.number }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CALENDAR_EMAIL_MAP: ${{ vars.CALENDAR_EMAIL_MAP }} run: | python - <<'PYTHON' import os, json, time, subprocess from google.oauth2 import service_account from googleapiclient.discovery import build - # GitHub 계정의 public email 조회 login = os.environ['ASSIGNEE_LOGIN'] + + # 1순위: GitHub 계정의 public email 조회 result = subprocess.run( ['gh', 'api', f'/users/{login}', '--jq', '.email'], capture_output=True, text=True, env={**os.environ} ) email = result.stdout.strip() - if not email or email == 'null': - print(f"Skipping {login}: public email not set on GitHub account") - exit(0) + if email == 'null': + email = '' + + # 2순위: GitHub public email이 없으면 CALENDAR_EMAIL_MAP variable 확인 + if not email: + email_map = json.loads(os.environ.get('CALENDAR_EMAIL_MAP') or '{}') + email = email_map.get(login, '') + if email: + print(f"Using mapped email for {login}: {email}") + else: + print(f"Skipping {login}: no email found. " + f"Add '{login}: ' to CALENDAR_EMAIL_MAP variable to enable.") + exit(0) key_info = json.loads(os.environ['GOOGLE_SERVICE_ACCOUNT_KEY']) credentials = service_account.Credentials.from_service_account_info(