Generate Profile Images #24
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: Generate Profile Images | |
| on: | |
| workflow_dispatch: # Manual trigger | |
| schedule: | |
| # Run at 00:00 UTC every day | |
| - cron: '0 0 * * *' | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| packages: read | |
| actions: read | |
| jobs: | |
| generate-images: | |
| runs-on: ubuntu-latest | |
| # Only run in the main organization repository, not in forks | |
| if: github.repository == 'Open-Dev-Society/openreadme' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| token: ${{ secrets.IMAGE_TOKEN || github.token }} | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Read user mappings | |
| id: user-mappings | |
| run: | | |
| MAPPINGS=$(jq -r 'to_entries | map("\(.key)=\(.value)") | join(" ")' data/user-mapping.json) | |
| echo "mappings=${MAPPINGS}" >> $GITHUB_OUTPUT | |
| - name: Configure Git | |
| run: | | |
| git config --global user.name "github-actions[bot]" | |
| git config --global user.email "github-actions@github.com" | |
| git config --global --add safe.directory /github/workspace | |
| - name: Stage any existing changes | |
| run: | | |
| git config --global --add safe.directory /github/workspace | |
| git add . | |
| - name: Commit any existing changes | |
| run: | | |
| git diff-index --quiet HEAD || git commit -m "chore: update generated files [skip ci]" | |
| - name: Pull latest changes | |
| run: git pull origin main --rebase | |
| - name: Generate profile images | |
| env: | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| npm install node-fetch@2 | |
| cat > generate-images.js << 'EOL' | |
| const fs = require('fs'); | |
| const fetch = require('node-fetch'); | |
| const path = require('path'); | |
| const API_URL = process.env.API_URL || 'https://openreadme.vercel.app/api/openreadme'; | |
| const GITHUB_TOKEN = process.env.GITHUB_TOKEN; | |
| const REPO_OWNER = 'Open-Dev-Society'; | |
| const REPO_NAME = 'openreadme'; | |
| // Read user profiles from JSON file | |
| function readUserProfiles() { | |
| try { | |
| const profilesPath = path.join(process.cwd(), 'data', 'user-profiles.json'); | |
| const data = fs.readFileSync(profilesPath, 'utf-8'); | |
| return JSON.parse(data); | |
| } catch (error) { | |
| console.error('⚠️ Error reading user profiles:', error.message); | |
| return {}; | |
| } | |
| } | |
| async function generateProfileImage(username, userId) { | |
| try { | |
| console.log(`🎨 Generating image for ${username} (${userId})...`); | |
| // Read stored user profile data first | |
| const userProfiles = readUserProfiles(); | |
| const storedProfile = userProfiles[username] || {}; | |
| console.log(`📦 Stored profile data:`, storedProfile); | |
| // Get user data from GitHub API as fallback | |
| const userResponse = await fetch(`https://api.github.com/users/${username}`, { | |
| headers: { | |
| 'Authorization': `token ${GITHUB_TOKEN}`, | |
| 'Accept': 'application/vnd.github.v3+json', | |
| 'User-Agent': 'OpenReadme-Workflow' | |
| } | |
| }); | |
| if (!userResponse.ok) { | |
| console.warn(`⚠️ GitHub API warning for ${username}: ${userResponse.statusText}`); | |
| } | |
| const userData = userResponse.ok ? await userResponse.json() : { login: username }; | |
| // Prioritize stored profile data over GitHub API data | |
| const name = storedProfile.name || userData.name || username; | |
| const profilePic = storedProfile.profilePic || userData.avatar_url || ''; | |
| const twitterUsername = storedProfile.twitterUsername || userData.twitter_username || ''; | |
| const linkedinUsername = storedProfile.linkedinUsername || ''; | |
| const portfolioUrl = storedProfile.portfolioUrl || userData.blog || userData.html_url || ''; | |
| console.log(`✅ Using data - Name: ${name}, Twitter: ${twitterUsername}, LinkedIn: ${linkedinUsername}`); | |
| // Build API URL with parameters | |
| const params = new URLSearchParams({ | |
| username: username, // Required for API validation | |
| n: name, | |
| i: profilePic, | |
| g: username, | |
| x: twitterUsername, | |
| l: linkedinUsername, | |
| p: portfolioUrl, | |
| t: 'classic' | |
| }); | |
| const apiUrl = `${API_URL}?${params.toString()}`; | |
| console.log(`📡 Calling API: ${apiUrl}`); | |
| const response = await fetch(apiUrl, { | |
| method: 'POST', | |
| headers: { | |
| 'Accept': 'application/json', | |
| 'Content-Type': 'application/json', | |
| 'User-Agent': 'OpenReadme-Workflow' | |
| } | |
| }); | |
| console.log(`📊 Response status: ${response.status} ${response.statusText}`); | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| console.error(`❌ API error response: ${errorText}`); | |
| throw new Error(`API error (${response.status}): ${errorText}`); | |
| } | |
| const result = await response.json(); | |
| console.log(`✅ Successfully generated image for ${username}`); | |
| console.log(`🔗 Image URL: ${result.url}`); | |
| console.log(`📝 Method: ${result.method}`); | |
| return result.url; | |
| } catch (error) { | |
| console.error(`❌ Error generating image for ${username}:`, error.message); | |
| console.error(`🔍 Stack trace:`, error.stack); | |
| return null; | |
| } | |
| } | |
| // Process all users | |
| (async () => { | |
| try { | |
| const mappingsString = process.env.MAPPINGS || ''; | |
| const mappings = mappingsString.split(' ').filter(m => m.trim()); | |
| console.log(`📋 Found ${mappings.length} users to process`); | |
| console.log(`🔧 Using API URL: ${API_URL}`); | |
| if (mappings.length === 0) { | |
| console.log('⚠️ No user mappings found to process'); | |
| return; | |
| } | |
| let successCount = 0; | |
| let errorCount = 0; | |
| for (const mapping of mappings) { | |
| if (!mapping.trim()) continue; | |
| const [username, userId] = mapping.split('='); | |
| if (!username || !userId) { | |
| console.warn(`⚠️ Invalid mapping format: ${mapping}`); | |
| continue; | |
| } | |
| console.log(`\n${'='.repeat(50)}`); | |
| console.log(`🔄 Processing ${username} (${userId})`); | |
| console.log(`${'='.repeat(50)}`); | |
| try { | |
| const imageUrl = await generateProfileImage(username, userId); | |
| if (imageUrl) { | |
| successCount++; | |
| console.log(`✅ Success for ${username}: ${imageUrl}`); | |
| } else { | |
| errorCount++; | |
| console.log(`❌ Failed for ${username}`); | |
| } | |
| // Add delay to avoid rate limiting (2 seconds between requests) | |
| console.log(`⏳ Waiting 2 seconds before next request...`); | |
| await new Promise(resolve => setTimeout(resolve, 2000)); | |
| } catch (error) { | |
| errorCount++; | |
| console.error(`💥 Error processing ${username}:`, error.message); | |
| } | |
| } | |
| console.log(`\n${'='.repeat(60)}`); | |
| console.log(`📊 WORKFLOW SUMMARY`); | |
| console.log(`${'='.repeat(60)}`); | |
| console.log(`✅ Successful: ${successCount}`); | |
| console.log(`❌ Failed: ${errorCount}`); | |
| console.log(`📋 Total: ${successCount + errorCount}`); | |
| console.log(`${'='.repeat(60)}`); | |
| } catch (error) { | |
| console.error('💥 Workflow failed:', error.message); | |
| process.exit(1); | |
| } | |
| })(); | |
| EOL | |
| MAPPINGS="${{ steps.user-mappings.outputs.mappings }}" \ | |
| API_URL="https://openreadme.vercel.app/api/openreadme" \ | |
| node generate-images.js | |
| - name: Commit and push changes | |
| if: github.repository == 'Open-Dev-Society/openreadme' && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN || secrets.IMAGE_TOKEN }} | |
| run: | | |
| git add . | |
| if ! git diff-index --quiet HEAD --; then | |
| git commit -m "chore: update profile images [skip ci]" | |
| git pull origin main --rebase || true | |
| git push https://${{ github.actor }}:$GITHUB_TOKEN@github.com/${{ github.repository }}.git HEAD:main | |
| fi |