Skip to content

Commit

Permalink
merge: branch 'feature-tracking' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
brenocq committed Dec 29, 2024
2 parents 971139d + d80006b commit 761891b
Show file tree
Hide file tree
Showing 9 changed files with 419 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/scripts/redirect_readme/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env/
__pycache__/
14 changes: 14 additions & 0 deletions .github/scripts/redirect_readme/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Use an official lightweight Python image
FROM python:3.9-slim

# Set the working directory in the container
WORKDIR /app

# Copy the current directory contents into the container
COPY . .

# Install dependencies
RUN pip install -r requirements.txt

# Run Gunicorn to serve Flask
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8080", "app:app"]
39 changes: 39 additions & 0 deletions .github/scripts/redirect_readme/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from flask import Flask, redirect, Response
import requests
import re

app = Flask(__name__)

# Google Cloud Storage base URL
GCS_BASE_URL = "https://storage.googleapis.com/implot3d"

# Route to handle discussion redirects
@app.route('/discussion_<int:discussion_id>')
def redirect_to_discussion(discussion_id):
# Construct the URL for the SVG file in GCS
svg_url = f"{GCS_BASE_URL}/discussion_{discussion_id}.svg"

# Fetch the SVG content from GCS
response = requests.get(svg_url)

if response.status_code == 200:
svg_content = response.text

# Extract the GitHub discussion URL from SVG comments
match = re.search(r'<!--\s*(https://github.com/brenocq/implot3d/discussions/\d+)\s*-->', svg_content)

if match:
discussion_url = match.group(1)
# Redirect to the extracted URL
return redirect(discussion_url, code=302)
else:
# Return 404 if no URL is found in the SVG
return Response("Discussion link not found in SVG.", status=404)
else:
# Return 404 if the SVG does not exist
return Response("SVG not found.", status=404)

# Default route
@app.route('/')
def home():
return "Hello from implot3d! This is the redirect service."
3 changes: 3 additions & 0 deletions .github/scripts/redirect_readme/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
flask==3.1.0
gunicorn==23.0.0
requests==2.32.3
3 changes: 3 additions & 0 deletions .github/scripts/update_readme/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.svg
.env/
*.json
1 change: 1 addition & 0 deletions .github/scripts/update_readme/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
google-cloud-storage==2.19.0
290 changes: 290 additions & 0 deletions .github/scripts/update_readme/update_readme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
import os
import requests
import html
from datetime import datetime
from collections import Counter
from google.cloud import storage

# GCloud
storage_client = storage.Client()
bucket = storage_client.get_bucket('implot3d')

# GitHub token
GITHUB_TOKEN = os.environ['GITHUB_TOKEN']
if not GITHUB_TOKEN:
raise ValueError("GITHUB_TOKEN environment variable is not set")

def generate_status_svg(label_text, label_color, count):
width = 140
height = 120
label_width = len(label_text) * 8 + 5

# Create SVG content
svg = f"""
<svg width="{width}" height="{height}" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
<feDropShadow dx="2" dy="2" stdDeviation="2" flood-color="black"/>
</filter>
</defs>
<!-- Card Background with Shadow -->
<rect x="5" y="5" width="{width-10}" height="{height-10}" rx="12" fill="#212830" filter="url(#shadow)"/>
<!-- Label -->
<g transform="translate({width/2}, 20)">
<rect x="{-label_width/2}" y="0" width="{label_width}" height="24" rx="12" fill="{label_color}" fill-opacity="0.2" stroke="{label_color}" stroke-width="0.5"/>
<text x="0" y="17" font-size="14" fill="{label_color}" font-family="Arial" text-anchor="middle">{label_text}</text>
</g>
<!-- Text -->
<text x="{width/2}" y="90" font-size="40" fill="#9198a1" font-family="Arial" text-anchor="middle">{count}</text>
</svg>
"""

return svg

def generate_discussion_svg(title, emoji, labels, category, upvotes, comments, author, created_at, last_comment_by, last_comment_at, discussion_url):
width = 820
height = 120
emoji_size = 20 # 16 font size equal 20x19 px
emoji_box_x = 30
emoji_box_size = 54
upvote_width = len(str(upvotes)) * 10 + 30
upvote_x_center = 760
upvote_rect_x = upvote_x_center - upvote_width / 2

# Format dates
created_at_formatted = datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%SZ").strftime("%d %b %Y")
last_comment_at_formatted = (
datetime.strptime(last_comment_at, "%Y-%m-%dT%H:%M:%SZ").strftime("%d %b %Y") if last_comment_at else None
)

# Build the contributor text
contributor_comment = f'<tspan style="text-decoration: underline;">{author}</tspan> started on {created_at_formatted}.'
if last_comment_by and last_comment_at_formatted:
contributor_comment = contributor_comment + f' Last comment by <tspan style="text-decoration: underline;">{last_comment_by}</tspan> on {last_comment_at_formatted}.'

# Create SVG content
svg = f"""
<svg width="{width}" height="{height}" xmlns="http://www.w3.org/2000/svg">
<!-- {discussion_url} -->
<defs>
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
<feDropShadow dx="2" dy="2" stdDeviation="2" flood-color="black"/>
</filter>
</defs>
<!-- Card Background with Shadow -->
<rect x="5" y="5" width="{width-10}" height="{height-10}" rx="12" fill="#212830" filter="url(#shadow)"/>
<!-- Emoji Icon -->
<rect x="{emoji_box_x}" y="{(height - emoji_box_size)/2}" width="{emoji_box_size}" height="{emoji_box_size}" rx="6" fill="#57606a"/>
<text x="{emoji_box_x + emoji_box_size/2 - emoji_size/2}" y="{(height + emoji_size)/2-4}" font-size="16">{emoji}</text>
<!-- Title -->
<text x="100" y="40" font-size="20" fill="#9198a1" font-family="Arial" font-weight="bold">{html.escape(title)}</text>
<!-- Labels -->
<g transform="translate(100, 50)">
"""
x_offset = 0
for label in labels:
label_width = len(label['text']) * 8 + 5
svg += f"""
<rect x="{x_offset}" y="0" width="{label_width}" height="24" rx="12" fill="{label['color']}" fill-opacity="0.2" stroke="{label['color']}" stroke-width="0.5"/>
<text x="{x_offset + label_width / 2}" y="17" font-size="14" fill="{label['color']}" font-family="Arial" text-anchor="middle">{label['text']}</text>
"""
x_offset += label_width + 10

svg += f"""
</g>
<!-- Username -->
<text x="100" y="95" font-size="14" fill="#9198a1" font-family="Arial">{contributor_comment}</text>
<!-- Upvote Button -->
<g transform="translate({upvote_rect_x}, 45)">
<rect x="0" y="-12" width="{upvote_width}" height="24" rx="12" fill="#478be6" fill-opacity="0.2" stroke="#478be6" stroke-width="0.5"/>
<path d="M3.47 7.78a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0l4.25 4.25a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018L9 4.81v7.44a.75.75 0 0 1-1.5 0V4.81L4.53 7.78a.75.75 0 0 1-1.06 0Z" fill="#478be6" transform="translate(5, -8) scale(1.14, 1.14)"/>
<text x="{upvote_width / 2 + 8}" y="6" font-size="16" fill="#478be6" font-family="Arial" text-anchor="middle">{upvotes}</text>
</g>
<!-- Comments -->
<g transform="translate(747, 75)">
<path d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" fill="#8b949e" transform="scale(1.14, 1.14)"/>
<text x="20" y="14" font-size="16" fill="#8b949e" font-family="Arial">{comments}</text>
</g>
</svg>
"""

return svg

def update_svgs():
url = "https://api.github.com/graphql"

headers = {
"Authorization": f"Bearer {GITHUB_TOKEN}",
"Content-Type": "application/json"
}

query = """
{
repository(owner: "brenocq", name: "implot3d") {
discussions(first: 100, categoryId: "DIC_kwDONQXA0M4ClSCg", orderBy: {field: UPDATED_AT, direction: DESC}) {
nodes {
title
url
createdAt
updatedAt
upvoteCount
comments(first: 10) {
totalCount
nodes {
author {
login
}
createdAt
replies(first: 10) {
totalCount
nodes {
author {
login
}
createdAt
}
}
}
}
labels(first: 5) {
nodes {
name
color
}
}
category {
name
}
author {
login
}
}
}
}
}
"""

response = requests.post(url, headers=headers, json={"query": query})
data = response.json()

if response.status_code == 200 and 'data' in data:
discussions = data["data"]["repository"]["discussions"]["nodes"]

############### Generate status SVGs ###############
# Count number of discussions by status
status_counter = Counter({
'status:idea': 0,
'status:todo': 0,
'status:doing': 0,
'status:review': 0,
'status:done': 0
})
for discussion in discussions:
labels = [label['name'] for label in discussion['labels']['nodes']]
for status in status_counter.keys():
if status in labels:
status_counter[status] += 1
# Generate SVGs for Each Status
status_colors = {
'status:idea': '#5DADE2',
'status:todo': '#3498DB',
'status:doing': '#F1C40F',
'status:review': '#E67E22',
'status:done': '#27AE60'
}

for status, count in status_counter.items():
print(f"Generating SVG for: {status}")

svg_status_output = generate_status_svg(
label_text=status,
label_color=status_colors[status],
count=count
)

# Save and Upload SVG
filename = f"{status.split(':')[1]}.svg"
with open(filename, "w") as f:
f.write(svg_status_output)
print(f"Saved SVG as {filename}")

# Upload SVG to GCloud
blob = bucket.blob(filename)
blob.upload_from_filename(filename)
print(f"Uploaded {filename} to google storage")
print("-" * 60)

############### Generate discussion SVGs ###############
# Generate SVGs for 5 most recent discussions
for i, discussion in enumerate(discussions):
if i >= 5:
break
print(f"Generating SVG for: {discussion['title']}")

# Calculate total comments (including replies)
total_comments = 0
last_comment_by = None
last_comment_at = None
for comment in discussion['comments']['nodes']:
total_comments += 1 # Top-level comment
total_comments += comment['replies']['totalCount'] # Add replies

# Track the last comment
if last_comment_at is None or comment['createdAt'] > last_comment_at:
last_comment_by = comment['author']['login']
last_comment_at = comment['createdAt']

# Track the last comment in case it is a reply
for reply in comment['replies']['nodes']:
if reply['createdAt'] > last_comment_at:
last_comment_by = reply['author']['login']
last_comment_at = reply['createdAt']

# Extract labels
labels = [
{"text": label['name'], "color": f"#{label['color']}"}
for label in discussion['labels']['nodes']
]

# Generate the SVG for each discussion
svg_output = generate_discussion_svg(
title=discussion['title'],
emoji="💡",
labels=labels,
category=discussion['category']['name'],
upvotes=discussion['upvoteCount'],
comments=total_comments,
author=discussion['author']['login'],
created_at=discussion['createdAt'],
last_comment_by=last_comment_by,
last_comment_at=last_comment_at,
discussion_url=discussion['url']
)

# Save each SVG to a unique file
filename = f"discussion_{i}.svg"
with open(filename, "w") as f:
f.write(svg_output)

print(f"Saved SVG as {filename}")

# Upload SVG to GCloud
blob = bucket.blob(filename)
blob.upload_from_filename(filename)
print(f"Uploaded {filename} to google storage")
print("-" * 60)
else:
print("Error or No Data Returned")
print(f"Response: {data}")

update_svgs()
39 changes: 39 additions & 0 deletions .github/workflows/update_readme.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Update README

on:
discussion:
types: [created, edited, labeled, unlabeled]
discussion_comment:
types: [created, edited]

jobs:
update-readme:
runs-on: ubuntu-latest
permissions:
discussions: read

steps:
- name: Checkout Repository
uses: actions/checkout@v3

- name: Set Up Python
uses: actions/setup-python@v3
with:
python-version: '3.x'

- name: Authenticate with GCP (Service Account Key)
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
echo "$GCP_SA_KEY" > /tmp/gcp-key.json
- name: Install Dependencies
run: |
pip install -r .github/scripts/update_readme/requirements.txt
- name: Update README images
env:
GITHUB_TOKEN: ${{ github.token }}
GOOGLE_APPLICATION_CREDENTIALS: "/tmp/gcp-key.json"
run: |
python .github/scripts/update_readme/update_readme.py
Loading

0 comments on commit 761891b

Please sign in to comment.