Skip to content

Commit e62f2f1

Browse files
authored
feat(data): add projects.json and reorganize README (#55)
* feat(data): add projects.json and reorganize README - Add data/projects.json as single source of truth for projects - Move documentation sites to their own category in README - JSON file supports Tool Reminder Tuesday and README generation * feat(social): add Tool Reminder Tuesday workflow - Runs every Tuesday at 12PM EDT (16:00 UTC) - Randomly selects a project from data/projects.json - Tracks history to avoid repeating projects until all featured - Excludes Documentation Sites category - Supports dry_run mode for testing - Posts to Bluesky with repo embed card * fix(social): use CONTRIBUTORS_TOKEN for history tracking
1 parent 19d1478 commit e62f2f1

3 files changed

Lines changed: 437 additions & 10 deletions

File tree

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
name: Tool Reminder Tuesday
2+
3+
on:
4+
schedule:
5+
# Every Tuesday at 12PM EDT (16:00 UTC) / 11AM EST
6+
- cron: '0 16 * * 2'
7+
workflow_dispatch:
8+
inputs:
9+
dry_run:
10+
description: 'Dry run (do not post)'
11+
required: false
12+
type: boolean
13+
default: false
14+
15+
jobs:
16+
select-project:
17+
runs-on: ubuntu-latest
18+
outputs:
19+
has_project: ${{ steps.pick.outputs.has_project }}
20+
project_name: ${{ steps.pick.outputs.project_name }}
21+
project_description: ${{ steps.pick.outputs.project_description }}
22+
project_repo: ${{ steps.pick.outputs.project_repo }}
23+
project_url: ${{ steps.pick.outputs.project_url }}
24+
project_emoji: ${{ steps.pick.outputs.project_emoji }}
25+
project_hashtags: ${{ steps.pick.outputs.project_hashtags }}
26+
post_text: ${{ steps.format.outputs.post_text }}
27+
steps:
28+
- name: Checkout
29+
uses: actions/checkout@v4
30+
31+
- name: Read history variable
32+
id: history
33+
env:
34+
GH_TOKEN: ${{ secrets.CONTRIBUTORS_TOKEN }}
35+
run: |
36+
HISTORY_VAR="TOOL_REMINDER_HISTORY"
37+
echo "Reading history from variable: $HISTORY_VAR"
38+
39+
# Get the variable value (returns empty if not found)
40+
HISTORY_JSON=$(gh api "repos/${{ github.repository }}/actions/variables/$HISTORY_VAR" --jq '.value' 2>/dev/null || echo '[]')
41+
42+
# Validate it's a JSON array, default to empty array if not
43+
if ! echo "$HISTORY_JSON" | python3 -c "import sys, json; json.load(sys.stdin)" 2>/dev/null; then
44+
HISTORY_JSON='[]'
45+
fi
46+
47+
echo "History contains $(echo "$HISTORY_JSON" | python3 -c "import sys, json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0) entries"
48+
echo "exclude_repos=$HISTORY_JSON" >> $GITHUB_OUTPUT
49+
50+
- name: Select random project
51+
id: pick
52+
run: |
53+
EXCLUDE_REPOS='${{ steps.history.outputs.exclude_repos }}'
54+
55+
PROJECT_JSON=$(python3 << 'PYEOF'
56+
import json
57+
import random
58+
import os
59+
60+
# Load projects
61+
with open('data/projects.json', 'r') as f:
62+
data = json.load(f)
63+
64+
# Load exclusion list
65+
exclude_repos = json.loads(os.environ.get('EXCLUDE_REPOS', '[]'))
66+
67+
# Flatten all projects, excluding Documentation Sites category
68+
all_projects = []
69+
for category in data['categories']:
70+
if category['name'] == 'Documentation Sites':
71+
continue
72+
for project in category['projects']:
73+
all_projects.append(project)
74+
75+
# Filter out recently selected projects
76+
eligible = [p for p in all_projects if p['repo'] not in exclude_repos]
77+
78+
# If all projects have been selected, reset and use all
79+
if not eligible:
80+
eligible = all_projects
81+
print(json.dumps({'reset': True}), file=open('/tmp/reset_flag', 'w'))
82+
83+
# Pick a random project
84+
if eligible:
85+
selected = random.choice(eligible)
86+
print(json.dumps(selected))
87+
else:
88+
print(json.dumps({}))
89+
PYEOF
90+
)
91+
92+
# Check if we got a project
93+
PROJECT_NAME=$(echo "$PROJECT_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin).get('name', ''))")
94+
95+
if [ -z "$PROJECT_NAME" ]; then
96+
echo "No eligible projects found"
97+
echo "has_project=false" >> $GITHUB_OUTPUT
98+
exit 0
99+
fi
100+
101+
echo "has_project=true" >> $GITHUB_OUTPUT
102+
103+
PROJECT_REPO=$(echo "$PROJECT_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin).get('repo', ''))")
104+
PROJECT_DESC=$(echo "$PROJECT_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin).get('description', ''))")
105+
PROJECT_EMOJI=$(echo "$PROJECT_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin).get('emoji', ''))")
106+
PROJECT_TOPICS=$(echo "$PROJECT_JSON" | python3 -c "import sys, json; print(' '.join(['#' + t for t in json.load(sys.stdin).get('topics', [])]))")
107+
PROJECT_URL="https://github.com/CodingWithCalvin/$PROJECT_REPO"
108+
109+
echo "Selected: $PROJECT_NAME ($PROJECT_REPO)"
110+
echo "Description: $PROJECT_DESC"
111+
echo "URL: $PROJECT_URL"
112+
echo "Hashtags: $PROJECT_TOPICS"
113+
114+
echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT
115+
echo "project_repo=$PROJECT_REPO" >> $GITHUB_OUTPUT
116+
echo "project_description=$PROJECT_DESC" >> $GITHUB_OUTPUT
117+
echo "project_emoji=$PROJECT_EMOJI" >> $GITHUB_OUTPUT
118+
echo "project_hashtags=$PROJECT_TOPICS" >> $GITHUB_OUTPUT
119+
echo "project_url=$PROJECT_URL" >> $GITHUB_OUTPUT
120+
env:
121+
EXCLUDE_REPOS: ${{ steps.history.outputs.exclude_repos }}
122+
123+
- name: Format post text
124+
id: format
125+
if: steps.pick.outputs.has_project == 'true'
126+
run: |
127+
NAME="${{ steps.pick.outputs.project_name }}"
128+
DESC="${{ steps.pick.outputs.project_description }}"
129+
EMOJI="${{ steps.pick.outputs.project_emoji }}"
130+
HASHTAGS="${{ steps.pick.outputs.project_hashtags }}"
131+
URL="${{ steps.pick.outputs.project_url }}"
132+
133+
# Format the post
134+
POST_TEXT=$(cat << EOF
135+
$EMOJI Tool Reminder Tuesday!
136+
137+
Check out $NAME - $DESC
138+
139+
$HASHTAGS
140+
EOF
141+
)
142+
143+
# Trim leading whitespace from heredoc
144+
POST_TEXT=$(echo "$POST_TEXT" | sed 's/^[[:space:]]*//')
145+
146+
echo "Post text:"
147+
echo "$POST_TEXT"
148+
149+
# Use delimiter for multiline output
150+
echo "post_text<<POSTEOF" >> $GITHUB_OUTPUT
151+
echo "$POST_TEXT" >> $GITHUB_OUTPUT
152+
echo "POSTEOF" >> $GITHUB_OUTPUT
153+
154+
- name: Update history variable
155+
if: steps.pick.outputs.has_project == 'true' && inputs.dry_run != true
156+
env:
157+
GH_TOKEN: ${{ secrets.CONTRIBUTORS_TOKEN }}
158+
run: |
159+
HISTORY_VAR="TOOL_REMINDER_HISTORY"
160+
NEW_REPO="${{ steps.pick.outputs.project_repo }}"
161+
CURRENT_HISTORY='${{ steps.history.outputs.exclude_repos }}'
162+
163+
# Check if we need to reset (all projects were selected)
164+
if [ -f /tmp/reset_flag ]; then
165+
echo "Resetting history - all projects have been featured"
166+
CURRENT_HISTORY='[]'
167+
fi
168+
169+
# Default to empty array if no history
170+
if [ -z "$CURRENT_HISTORY" ]; then
171+
CURRENT_HISTORY='[]'
172+
fi
173+
174+
# Add new repo to history
175+
UPDATED_HISTORY=$(python3 -c "
176+
import json
177+
history = json.loads('$CURRENT_HISTORY')
178+
new_repo = '$NEW_REPO'
179+
180+
# Add new repo to the list
181+
if new_repo and new_repo not in history:
182+
history.append(new_repo)
183+
184+
print(json.dumps(history))
185+
")
186+
187+
echo "Updating history variable with $(echo "$UPDATED_HISTORY" | python3 -c "import sys, json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0) entries"
188+
189+
# Check if variable exists
190+
if gh api "repos/${{ github.repository }}/actions/variables/$HISTORY_VAR" --silent 2>/dev/null; then
191+
# Update existing variable
192+
gh api --method PATCH "repos/${{ github.repository }}/actions/variables/$HISTORY_VAR" \
193+
-f value="$UPDATED_HISTORY"
194+
else
195+
# Create new variable
196+
gh api --method POST "repos/${{ github.repository }}/actions/variables" \
197+
-f name="$HISTORY_VAR" \
198+
-f value="$UPDATED_HISTORY"
199+
fi
200+
201+
echo "History updated successfully"
202+
203+
post-to-bluesky:
204+
needs: select-project
205+
if: needs.select-project.outputs.has_project == 'true' && inputs.dry_run != true
206+
uses: ./.github/workflows/bluesky-post.yml
207+
with:
208+
post_text: ${{ needs.select-project.outputs.post_text }}
209+
embed_url: ${{ needs.select-project.outputs.project_url }}
210+
secrets:
211+
BLUESKY_USERNAME: ${{ secrets.BLUESKY_USERNAME }}
212+
BLUESKY_APP_PASSWORD: ${{ secrets.BLUESKY_APP_PASSWORD }}
213+
214+
dry-run-summary:
215+
needs: select-project
216+
if: needs.select-project.outputs.has_project == 'true' && inputs.dry_run == true
217+
runs-on: ubuntu-latest
218+
steps:
219+
- name: Show what would be posted
220+
run: |
221+
echo "## Dry Run Summary" >> $GITHUB_STEP_SUMMARY
222+
echo "" >> $GITHUB_STEP_SUMMARY
223+
echo "**Selected Project:** ${{ needs.select-project.outputs.project_name }}" >> $GITHUB_STEP_SUMMARY
224+
echo "" >> $GITHUB_STEP_SUMMARY
225+
echo "**Repository:** ${{ needs.select-project.outputs.project_url }}" >> $GITHUB_STEP_SUMMARY
226+
echo "" >> $GITHUB_STEP_SUMMARY
227+
echo "### Post Preview" >> $GITHUB_STEP_SUMMARY
228+
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
229+
echo "${{ needs.select-project.outputs.post_text }}" >> $GITHUB_STEP_SUMMARY
230+
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY

0 commit comments

Comments
 (0)