Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/google-sheet-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: "Google Sheet Sync"

on:
schedule:
- cron: "0 0 * * *" # Run once a day at midnight UTC
workflow_dispatch:
inputs:
spreadsheet_id:
description: "The ID of the Google Sheet to monitor."
required: true
stop_time:
description: "The UTC time (HH:MM) at which the action should stop."
required: true
stop_day:
description: "The day (YYYY-MM-DD) on which the action should stop."
required: true

jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.x"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install google-api-python-client PyGithub google-auth

- name: Create Google Credentials File
env:
GOOGLE_CREDENTIALS: ${{ secrets.GOOGLE_CREDENTIALS }}
run: |
echo "${GOOGLE_CREDENTIALS}" > google-credentials.json
Comment on lines +35 to +39
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before merging, let's double-check that this step does not write the credentials to the Action logs. I'm 99.9% sure it won't print to the logs since it's a secret, but better safe than sorry 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK just verified that it does not - we good!


- name: Run Google Sheet Sync
env:
INPUT_SPREADSHEET_ID: ${{ github.event.inputs.spreadsheet_id }}
INPUT_STOP_TIME: ${{ github.event.inputs.stop_time }}
INPUT_STOP_DAY: ${{ github.event.inputs.stop_day }}
GITHUB_TOKEN: ${{ secrets.RFC_TOKEN }}
run: python google-sheet-sync/sync.py
15 changes: 15 additions & 0 deletions google-sheet-sync/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: "Google Sheet Sync for RFC Integration"
description: "Syncs a Google Sheet with a GitHub repository as a CSV for the purpose of RFC Integration."
inputs:
spreadsheet_id:
description: "The ID of the Google Sheet to monitor."
required: true
stop_time:
description: "The UTC time (HH:MM) at which the action should stop."
required: true
stop_day:
description: "The day (YYYY-MM-DD) on which the action should stop."
required: true
runs:
using: "python"
main: "sync.py"
138 changes: 138 additions & 0 deletions google-sheet-sync/sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import os
import csv
from io import StringIO
from datetime import datetime
from google.oauth2.service_account import Credentials
from googleapiclient.discovery import build
Comment on lines +5 to +6
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if you'd find the effort to switch over worth it, but I really like using gspread library to access and edit Google spreadsheets!

It could make some of the functions below a bit more readable and easier to comprehend right away.

from github import Github, GithubException

def authenticate_google(credentials_file: str) -> build:
"""
Authenticate and create a Google Sheets API client.

Args:
credentials_file (str): Path to the Google service account JSON credentials file.

Returns:
googleapiclient.discovery.Resource: Authenticated client for interacting with Google Sheets API.
"""
creds = Credentials.from_service_account_file(credentials_file, scopes=["https://www.googleapis.com/auth/spreadsheets.readonly"])
return build('sheets', 'v4', credentials=creds)

def get_sheet_name(service, spreadsheet_id: str) -> str:
"""
Retrieve the title of the Google Sheet and format it for use as a branch name.

Args:
service (googleapiclient.discovery.Resource): Authenticated Google Sheets API client.
spreadsheet_id (str): ID of the Google Sheet.

Returns:
str: Formatted sheet title with spaces replaced by dashes.
"""
sheet_metadata = service.spreadsheets().get(spreadsheetId=spreadsheet_id).execute()
return sheet_metadata.get("properties", {}).get("title", "UntitledSheet").replace(" ", "-")

def get_sheet_data_as_csv(service, spreadsheet_id: str, range: str = "A1:Z1000") -> str:
"""
Retrieve data from the specified Google Sheet range and return it as a CSV string.

Args:
service (googleapiclient.discovery.Resource): Authenticated Google Sheets API client.
spreadsheet_id (str): ID of the Google Sheet.
range (str, optional): Cell range to fetch data from. Defaults to "A1:Z1000".

Returns:
str: CSV formatted data from the sheet, or None if the sheet is empty.
"""
sheet = service.spreadsheets()
result = sheet.values().get(spreadsheetId=spreadsheet_id, range=range).execute()
data = result.get('values', [])
if not data:
return None
output = StringIO()
writer = csv.writer(output)
writer.writerows(data)
output.seek(0)
return output.read()

def main():
spreadsheet_id = os.getenv("INPUT_SPREADSHEET_ID")
stop_time = os.getenv("INPUT_STOP_TIME")
stop_day = os.getenv("INPUT_STOP_DAY")
github_token = os.getenv("GITHUB_TOKEN")
repo_name = os.getenv("GITHUB_REPOSITORY")
credentials_file = "./google-credentials.json" # Path to dynamically created credentials file

service = authenticate_google(credentials_file)
sheet_name = get_sheet_name(service, spreadsheet_id)
branch_name = f"RFC-{sheet_name}"
file_path = f"RFC_DATA/{sheet_name}.csv"

github = Github(github_token)
repo = github.get_repo(repo_name)

current_day = datetime.utcnow().strftime("%Y-%m-%d")
current_time = datetime.utcnow().strftime("%H:%M")

if current_day > stop_day or (current_day == stop_day and current_time > stop_time):
print(f"Current day/time ({current_day} {current_time}) has exceeded the stop conditions ({stop_day} {stop_time}). Exiting.")
return

csv_content = get_sheet_data_as_csv(service, spreadsheet_id)
if not csv_content:
print("No data fetched from Google Sheet. Exiting.")
return

# Ensure branch exists
try:
repo.get_branch(branch_name)
except GithubException as e:
if e.status == 404:
main_branch = repo.get_branch("main")
repo.create_git_ref(ref=f"refs/heads/{branch_name}", sha=main_branch.commit.sha)
else:
raise

# Commit changes to the branch
try:
try:
# Attempt to get the file contents
contents = repo.get_contents(file_path, ref=branch_name)
# Update the file if it exists
repo.update_file(
file_path,
"Update CSV file from Google Sheet",
csv_content,
contents.sha,
branch=branch_name
)
except GithubException as e:
# If the file doesn't exist (404), create it
if e.status == 404:
repo.create_file(
file_path,
"Create CSV file from Google Sheet",
csv_content,
branch=branch_name
)
else:
raise # Re-raise other exceptions
except Exception as e:
print(f"Error committing changes: {e}")

# Create or reuse PR
try:
open_prs = repo.get_pulls(state="open", head=f"{repo.owner.login}:{branch_name}")
if open_prs.totalCount == 0:
repo.create_pull(
title=f"{branch_name} Updates",
body="This PR contains updates from the linked Google Sheet.",
head=branch_name,
base="main",
)
except Exception as e:
print(f"Error creating PR: {e}")

if __name__ == "__main__":
main()