Content Submission: Go Design Patterns #43
Workflow file for this run
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: Payload-Based Content PR Handler | |
| on: | |
| pull_request: | |
| types: [opened, edited, synchronize] | |
| paths: | |
| - '.github/content-payloads/**' | |
| - 'base.json' | |
| pull_request_review: | |
| types: [submitted] | |
| jobs: | |
| validate-payload-pr: | |
| runs-on: ubuntu-latest | |
| if: github.event.pull_request.draft == false | |
| permissions: | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| persist-credentials: true | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '18' | |
| - name: Install dependencies | |
| run: | | |
| npm install -g marked js-yaml | |
| - name: Check if this is a payload PR | |
| run: | | |
| echo "🔍 Checking if this is a payload-based PR..." | |
| # Check if payloads directory exists and has files | |
| if [ ! -d ".github/content-payloads" ]; then | |
| echo "ℹ️ No payloads directory found - this is not a payload PR" | |
| echo "SKIP_PAYLOAD_VALIDATION=true" >> $GITHUB_ENV | |
| exit 0 | |
| fi | |
| payload_files=$(find .github/content-payloads -name "*.json" 2>/dev/null | wc -l) | |
| if [ "$payload_files" -eq 0 ]; then | |
| echo "ℹ️ No payload files found - this is not a payload PR" | |
| echo "SKIP_PAYLOAD_VALIDATION=true" >> $GITHUB_ENV | |
| exit 0 | |
| fi | |
| echo "✅ Payload files found - proceeding with validation" | |
| echo "SKIP_PAYLOAD_VALIDATION=false" >> $GITHUB_ENV | |
| - name: Handle non-payload PR | |
| if: env.SKIP_PAYLOAD_VALIDATION == 'true' | |
| run: | | |
| echo "ℹ️ This is not a payload-based PR - skipping payload validation" | |
| echo "✅ Workflow completed successfully for non-payload PR" | |
| - name: Validate payload files | |
| if: env.SKIP_PAYLOAD_VALIDATION != 'true' | |
| run: | | |
| echo "🔍 Validating payload files..." | |
| python3 << 'EOF' | |
| import json | |
| import os | |
| import sys | |
| import subprocess | |
| payloads_dir = '.github/content-payloads' | |
| if not os.path.exists(payloads_dir): | |
| print("❌ No payloads directory found") | |
| sys.exit(1) | |
| # Get only changed files in this PR | |
| try: | |
| result = subprocess.run(['git', 'diff', '--name-only', 'HEAD~1', 'HEAD'], | |
| capture_output=True, text=True, check=True) | |
| changed_files = result.stdout.strip().split('\n') | |
| payload_files = [f for f in changed_files | |
| if f.startswith('.github/content-payloads/') and f.endswith('.json')] | |
| except subprocess.CalledProcessError: | |
| # Fallback: process all files if not in PR context | |
| payload_files = [f for f in os.listdir(payloads_dir) if f.endswith('.json')] | |
| payload_files = [os.path.join(payloads_dir, f) for f in payload_files] | |
| if not payload_files: | |
| print("❌ No new payload files found in this PR") | |
| sys.exit(1) | |
| print(f"📋 Found {len(payload_files)} new payload file(s) in this PR") | |
| for payload_file in payload_files: | |
| try: | |
| # Handle both relative and absolute paths | |
| if payload_file.startswith(payloads_dir): | |
| file_path = payload_file | |
| else: | |
| file_path = os.path.join(payloads_dir, payload_file) | |
| with open(file_path, 'r') as f: | |
| payload = json.load(f) | |
| # Validate required fields | |
| required_fields = ['version', 'type', 'metadata', 'content'] | |
| for field in required_fields: | |
| if field not in payload: | |
| print(f"❌ Missing required field '{field}' in {payload_file}") | |
| sys.exit(1) | |
| # Validate metadata | |
| required_metadata = ['title', 'category', 'subtopic'] | |
| for field in required_metadata: | |
| if field not in payload['metadata']: | |
| print(f"❌ Missing required metadata field '{field}' in {payload_file}") | |
| sys.exit(1) | |
| # Validate content | |
| required_content = ['id', 'title', 'description'] | |
| for field in required_content: | |
| if field not in payload['content']: | |
| print(f"❌ Missing required content field '{field}' in {payload_file}") | |
| sys.exit(1) | |
| print(f"✅ Validated payload: {payload_file}") | |
| except Exception as e: | |
| print(f"❌ Error validating {payload_file}: {e}") | |
| sys.exit(1) | |
| print("✅ All payload files are valid") | |
| EOF | |
| - name: Validate base configuration | |
| if: env.SKIP_PAYLOAD_VALIDATION != 'true' | |
| run: | | |
| echo "🔍 Validating base configuration..." | |
| if [ -f "base.json" ]; then | |
| python3 -m json.tool base.json > /dev/null | |
| echo "✅ Base configuration is valid" | |
| else | |
| echo "❌ Base configuration not found" | |
| exit 1 | |
| fi | |
| - name: Test content merge engine | |
| if: env.SKIP_PAYLOAD_VALIDATION != 'true' | |
| run: | | |
| echo "🔄 Testing content merge engine..." | |
| node scripts/merge-content.js | |
| if [ -f "content-config.json" ]; then | |
| echo "✅ Content merge engine generated configuration successfully" | |
| else | |
| echo "❌ Content merge engine failed to generate configuration" | |
| exit 1 | |
| fi | |
| - name: Validate GitHub repository access | |
| if: env.SKIP_PAYLOAD_VALIDATION != 'true' | |
| run: | | |
| echo "🔍 Validating GitHub repository access..." | |
| python3 << 'EOF' | |
| import json | |
| import requests | |
| import sys | |
| import os | |
| import subprocess | |
| payloads_dir = '.github/content-payloads' | |
| # Get only changed files in this PR | |
| try: | |
| result = subprocess.run(['git', 'diff', '--name-only', 'HEAD~1', 'HEAD'], | |
| capture_output=True, text=True, check=True) | |
| changed_files = result.stdout.strip().split('\n') | |
| payload_files = [f for f in changed_files | |
| if f.startswith('.github/content-payloads/') and f.endswith('.json')] | |
| except subprocess.CalledProcessError: | |
| # Fallback: process all files if not in PR context | |
| payload_files = [f for f in os.listdir(payloads_dir) if f.endswith('.json')] | |
| payload_files = [os.path.join(payloads_dir, f) for f in payload_files] | |
| for payload_file in payload_files: | |
| # Handle both relative and absolute paths | |
| if payload_file.startswith(payloads_dir): | |
| file_path = payload_file | |
| else: | |
| file_path = os.path.join(payloads_dir, payload_file) | |
| with open(file_path, 'r') as f: | |
| payload = json.load(f) | |
| if 'repo' in payload['content'] and 'path' in payload['content']: | |
| repo = payload['content']['repo'] | |
| path = payload['content']['path'] | |
| # Skip validation if repo or path is empty | |
| if not repo or not path: | |
| print(f"⚠️ Skipping validation for {payload_file}: empty repo or path") | |
| continue | |
| # Test GitHub API access | |
| url = f"https://api.github.com/repos/{repo}" | |
| try: | |
| response = requests.get(url, timeout=10) | |
| if response.status_code == 200: | |
| print(f"✅ Repository accessible: {repo}") | |
| else: | |
| print(f"❌ Repository not accessible: {repo} (Status: {response.status_code})") | |
| sys.exit(1) | |
| except requests.RequestException as e: | |
| print(f"❌ Error checking repository {repo}: {e}") | |
| sys.exit(1) | |
| # Test file access | |
| raw_url = f"https://raw.githubusercontent.com/{repo}/main/{path}" | |
| try: | |
| file_response = requests.head(raw_url, timeout=10) | |
| if file_response.status_code == 200: | |
| print(f"✅ File accessible: {repo}/{path}") | |
| else: | |
| print(f"❌ File not accessible: {repo}/{path} (Status: {file_response.status_code})") | |
| sys.exit(1) | |
| except requests.RequestException as e: | |
| print(f"❌ Error checking file {repo}/{path}: {e}") | |
| sys.exit(1) | |
| else: | |
| print(f"⚠️ Skipping repository validation for {payload_file}: no repo/path specified") | |
| EOF | |
| - name: Generate site preview | |
| if: env.SKIP_PAYLOAD_VALIDATION != 'true' | |
| run: | | |
| echo "🏗️ Generating site preview with merged content..." | |
| node scripts/update-navigation.js | |
| echo "✅ Site preview generated successfully" | |
| # Verify the HTML files were updated with new content | |
| echo "🔍 Verifying updated HTML files..." | |
| node -e " | |
| const fs = require('fs'); | |
| const config = JSON.parse(fs.readFileSync('content-config.json', 'utf8')); | |
| // Get all new content titles from the merged config | |
| const newContentTitles = []; | |
| for (const category of Object.values(config.categories)) { | |
| for (const subtopic of Object.values(category.subtopics)) { | |
| for (const content of subtopic.content) { | |
| if (content.status === 'active') { | |
| newContentTitles.push(content.title); | |
| } | |
| } | |
| } | |
| } | |
| // Check if any HTML files contain the new content | |
| const htmlFiles = ['index.html', 'kubernetes.html', 'algorithms.html', 'networking.html', 'databases.html', 'microservices.html', 'system-design.html']; | |
| let foundContent = 0; | |
| for (const htmlFile of htmlFiles) { | |
| if (fs.existsSync(htmlFile)) { | |
| const content = fs.readFileSync(htmlFile, 'utf8'); | |
| for (const title of newContentTitles) { | |
| if (content.includes(title)) { | |
| foundContent++; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| if (foundContent > 0) { | |
| console.log('✅ HTML files updated with new content'); | |
| } else { | |
| console.log('❌ HTML files not updated properly'); | |
| process.exit(1); | |
| } | |
| " | |
| - name: Commit generated files | |
| if: env.SKIP_PAYLOAD_VALIDATION != 'true' | |
| run: | | |
| echo "📝 Committing generated HTML files to PR..." | |
| git config --local user.email "action@github.com" | |
| git config --local user.name "GitHub Action" | |
| # Check if there are changes to commit | |
| if git diff --quiet && git diff --cached --quiet; then | |
| echo "No changes to commit" | |
| else | |
| git add *.html content-config.json | |
| git commit -m "chore: Auto-generate HTML files with new content | |
| - Generated by payload workflow | |
| - Includes updated navigation and content | |
| - Auto-commit for Vercel preview deployment" | |
| echo "🔄 Pushing generated files to PR branch..." | |
| git push origin HEAD:${{ github.head_ref }} | |
| fi | |
| - name: Upload preview artifacts | |
| if: env.SKIP_PAYLOAD_VALIDATION != 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: site-preview | |
| path: | | |
| index.html | |
| kubernetes.html | |
| algorithms.html | |
| networking.html | |
| databases.html | |
| microservices.html | |
| system-design.html | |
| content-config.json | |
| retention-days: 7 | |
| - name: Comment on PR | |
| if: env.SKIP_PAYLOAD_VALIDATION != 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| try { | |
| // Read the generated content config | |
| const config = JSON.parse(fs.readFileSync('content-config.json', 'utf8')); | |
| // Count new payloads in this PR | |
| const { execSync } = require('child_process'); | |
| let payloadCount = 0; | |
| try { | |
| const changedFiles = execSync('git diff --name-only HEAD~1 HEAD', { encoding: 'utf8' }); | |
| const payloadFiles = changedFiles.trim().split('\n') | |
| .filter(file => file.startsWith('.github/content-payloads/') && file.endsWith('.json')); | |
| payloadCount = payloadFiles.length; | |
| } catch (error) { | |
| // Fallback: count all payload files | |
| const payloadsDir = '.github/content-payloads'; | |
| if (fs.existsSync(payloadsDir)) { | |
| const files = fs.readdirSync(payloadsDir); | |
| payloadCount = files.filter(f => f.endsWith('.json')).length; | |
| } | |
| } | |
| // Generate statistics | |
| let totalContent = 0; | |
| let totalVisualizations = 0; | |
| let totalGuides = 0; | |
| let categoriesWithContent = 0; | |
| for (const category of Object.values(config.categories)) { | |
| if (Object.keys(category.subtopics).length > 0) { | |
| categoriesWithContent++; | |
| for (const subtopic of Object.values(category.subtopics)) { | |
| for (const contentItem of subtopic.content) { | |
| if (contentItem.status === 'active') { | |
| totalContent++; | |
| if (contentItem.type === 'visualization') { | |
| totalVisualizations++; | |
| } else { | |
| totalGuides++; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| const comment = `## 🎉 Payload-Based Content Validation Successful! | |
| Your content payload(s) have been validated and merged successfully! | |
| ### 📊 Impact Summary | |
| - **Payload files processed**: ${payloadCount} | |
| - **Total content items**: ${totalContent} | |
| - **Visualizations**: ${totalVisualizations} | |
| - **Guides**: ${totalGuides} | |
| - **Active categories**: ${categoriesWithContent} | |
| - **Configuration version**: ${config.version} | |
| ### ✅ Validation Results | |
| - Payload file structure is valid | |
| - Base configuration is valid | |
| - Content merge engine executed successfully | |
| - GitHub repository access confirmed | |
| - File paths are accessible | |
| - Site preview generated successfully | |
| ### 🏗️ Site Preview Generated | |
| The workflow has generated a complete site preview with your changes: | |
| - Updated navigation structure | |
| - Modified category pages | |
| - Updated statistics | |
| - Preview artifacts uploaded for review | |
| ### 🚀 Next Steps | |
| Once this PR is merged, the content will be automatically: | |
| 1. Merged into the base configuration | |
| 2. Integrated into the site navigation | |
| 3. Made accessible through the template renderer | |
| 4. Included in site statistics | |
| 5. Deployed live immediately | |
| **Review the generated preview artifacts to see exactly how the site will look!** 🎯`; | |
| github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: comment | |
| }); | |
| } catch (error) { | |
| console.error('Error creating comment:', error); | |
| } | |
| auto-merge-ready: | |
| runs-on: ubuntu-latest | |
| needs: validate-payload-pr | |
| if: github.event.pull_request.draft == false && github.event.review.state == 'approved' | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Auto-merge PR | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const { owner, repo, number } = context.issue; | |
| // Check if PR is ready for merge | |
| const pr = await github.rest.pulls.get({ | |
| owner, | |
| repo, | |
| pull_number: number | |
| }); | |
| if (pr.data.mergeable && pr.data.mergeable_state === 'clean') { | |
| await github.rest.pulls.merge({ | |
| owner, | |
| repo, | |
| pull_number: number, | |
| merge_method: 'squash' | |
| }); | |
| console.log('✅ PR merged successfully!'); | |
| } else { | |
| console.log('❌ PR is not ready for merge'); | |
| } |