portfolio-auto

$npx mdskill add EliasOulkadi/shokunin/portfolio-auto

Auto-sync GitHub repos to portfolio website with screenshots.

  • Batch syncs GitHub repos to update portfolio projects.
  • Uses GitHub API and Playwright for screenshots.
  • Filters repos based on user-provided criteria.
  • Updates projects-data.js or Supabase database.

SKILL.md

.github/skills/portfolio-autoView on GitHub ↗
---
name: portfolio-auto
description: Auto-sync GitHub repos to portfolio website. Scans GitHub repos, captures screenshots with Playwright, generates project entries, and updates projects-data.js or Supabase DB. Use when user asks to "update portfolio", "sync projects", "add my repos to portfolio", or "refresh portfolio projects". Do NOT use for one-time project additions — batch sync only.
triggers:
  - "update portfolio"
  - "sync projects"
  - "add my repos"
  - "refresh portfolio"
  - "portfolio automation"
  - "GitHub sync"
  - "project screenshots"
negatives:
  - "one-time project"
  - "manual addition"
  - "design portfolio"
  - "PDF portfolio"
license: MIT
compatibility: opencode
metadata:
  workflow: automation
  audience: developers
  version: "2.0.0"
---


> **Note:** `last-sync.json` state file is auto-created on first successful sync. Ignore "missing file" warnings on first run.

# Portfolio Auto-Sync

Automatically sync GitHub repositories to your portfolio website.

## Workflow

### Step 1: Gather configuration

Ask the user:
- **GitHub username**: (default from git config)
- **Portfolio type**: `static` (projects-data.js) or `supabase` (API)
- **Portfolio directory**: Path to portfolio project
- **Filters**: Exclude repos (archived, forks, specific names)

### Step 2: Fetch repos via GitHub API

```
GET /users/{username}/repos?per_page=100&sort=updated&direction=desc
```

Extract: `name`, `description`, `html_url`, `homepage`, `language`, `topics`, `updated_at`

Filter out:
- Forks (unless user opts in)
- Profile repos (`{username}/{username}`)
- Archived repos

### Step 3: Detect changes

Compare against `last-sync.json` (stored in skill directory).

- **New repos**: Not in last sync → full process
- **Updated repos**: `updated_at` changed → re-screenshot
- **Unchanged repos**: Skip

### Step 4: Capture screenshots

For repos with a `homepage` or deploy URL:

```javascript
const { chromium } = require('playwright');
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto(process.env.URL, { waitUntil: 'networkidle', timeout: 15000 });
await page.screenshot({ path: `/tmp/portfolio-${name}.png`, fullPage: false });
await browser.close();
```

Save to portfolio's screenshot directory.

### Step 5: Update portfolio data

#### Static (projects-data.js)

```javascript
{
  title: '{repo.name}',
  description: 'Auto-generated: {description}',
  tech: '{language},{topics}',
  github_url: '{html_url}',
  live_url: '{homepage || ""}',
  featured: false
}
```

#### Supabase

```javascript
const res = await fetch('https://your-site.com/api/projects', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer {token}' },
  body: JSON.stringify(projectData)
});
```

### Step 6: Save sync state

```json
{
  "lastSync": "2026-05-12T18:30:00Z",
  "repos": {
    "example-repo": { "updated_at": "2026-04-20T10:00:00Z", "screenshot": true }
  }
}
```

### Step 7: Report results

```
Sync complete:
+ 2 new (Cyberian, Image Enhancer)
+ 1 screenshot captured
+ 1 updated (Portfolio)
- 0 errors
```

## Notes

- Screenshots require Playwright + valid URL. Skip if no homepage.
- Never change `featured: true` on existing projects — only user can promote.
- Schedule via cron/GitHub Actions for weekly sync.

## Error Handling

| Cause | Fix |
|-------|-----|
| GitHub API returns 401 Unauthorized | No token or expired token. Set `GITHUB_TOKEN` environment variable. Generate a new Personal Access Token at github.com/settings/tokens with `public_repo` scope. |
| GitHub API rate limit exceeded (403) | Unauthenticated requests are limited to 60/hour. Authenticated: 5000/hour. Add token. If already authenticated, wait for reset (check `X-RateLimit-Reset` header). |
| Repository has no `homepage` field and no deploy URL | Cannot capture a screenshot. Mark the repo as "no live preview" in the output. Use the GitHub URL as fallback link. |
| Playwright browser fails to launch | Chromium not installed. Run `npx playwright install chromium`. On headless server, add `--no-sandbox` flag: `chromium.launch({ headless: true, args: ['--no-sandbox'] })`. |
| Screenshot URL returns 404 or times out after 15s | Deploy URL is stale or site is down. Skip screenshot for this repo. Log the URL and error to a `failed-screenshots.json` file for manual review. |
| `projects-data.js` has custom formatting that auto-generation would break | A regex-based insert could corrupt handwritten code. Read the file first to detect non-standard structure. If detected, append a comment block with the new entries and ask user to merge manually. |
| Supabase API insert fails with foreign key or schema constraint | Portfolio schema might differ from the auto-generated payload shape. Read the Supabase table schema first. Map fields explicitly (not spread operator). Validate with a dry-run POST that returns 200 before committing. |
| `last-sync.json` is corrupted or manually edited | Parse as JSON in a try-catch. On failure, rename to `last-sync.json.bak` and start fresh sync from scratch. Warn user that incremental detection is lost. |

## GitHub API Patterns

### Repository Discovery
```python
import requests, os

headers = {"Authorization": f"token {os.environ['GITHUB_TOKEN']}"}

# Fetch all repos (paginated)
repos = []
page = 1
while True:
    r = requests.get(f"https://api.github.com/user/repos?per_page=100&page={page}&sort=updated", headers=headers)
    batch = r.json()
    if not batch: break
    repos.extend(batch)
    page += 1

# Filter: only non-fork, non-archived, has description
candidates = [r for r in repos if not r["fork"] and not r["archived"] and r["description"]]
```

### Rate Limiting
- GitHub API: 5,000 requests/hour (authenticated). Check `X-RateLimit-Remaining` header.
- For repos with 500+ repos: use conditional requests (`If-None-Match` with ETag)
- Sleep 1s between screenshot captures (avoid GitHub rate limit on assets)

### Playwright Screenshot Pattern
```python
from playwright.sync_api import sync_playwright

def capture(url, output_path):
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page(viewport={"width": 1280, "height": 800})
        page.goto(url, timeout=15000, wait_until="networkidle")
        page.screenshot(path=output_path, full_page=False)
        browser.close()
```

### Error Recovery
- Timeout on page load -> retry once, skip if still fails (mark in output)
- Private repo -> skip (cannot screenshot without auth)
- Page crashes -> capture error state as screenshot
- DNS failure -> skip with log message

### Caching Strategy
```python
import hashlib, json, os
cache_file = ".sync-cache.json"
cache = json.load(open(cache_file)) if os.path.exists(cache_file) else {}

for repo in repos:
    key = hashlib.md5(f"{repo['full_name']}:{repo['pushed_at']}".encode()).hexdigest()
    if cache.get(repo['full_name']) == key:
        continue  # Skip: no changes since last sync
    # ... capture screenshot, update entry ...
    cache[repo['full_name']] = key

json.dump(cache, open(cache_file, 'w'))
```

## Checklist

- [ ] GitHub token has correct scopes (repo, read:user) before fetching repos
- [ ] Screenshot runs only for repos with valid homepage URLs (HEAD check first)
- [ ] Playwright browser closed in finally block to prevent orphan processes
- [ ] Existing portfolio customizations preserved before upserting new entries
- [ ] Dry-run mode available to preview changes without executing

## Sources

- GitHub REST API v3 documentation (docs.github.com/en/rest/repos/repos) — repository listing, filtering, and pagination
- Playwright documentation (playwright.dev/docs/api/class-playwright) — headless browser launch, screenshot, and viewport configuration
- Supabase JavaScript client documentation (supabase.com/docs/reference/javascript) — API insert, upsert, and auth patterns
- Node.js `fs` module documentation (nodejs.org/api/fs.html) — file read/write with atomic rename for sync state safety
- Personal Access Token best practices (docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) — scopes, expiration, and secure storage
- GitHub Actions cron scheduling (docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule) — weekly sync automation pattern
- "Automate the Boring Stuff with Python" by Al Sweigart (No Starch Press, 2nd Edition, 2019) — automation reliability patterns applicable to JavaScript automation tasks

## Anti-Patterns

| Pattern | Problem | Fix |
|---------|---------|-----|
| Syncing without checking for existing customizations | Auto-generated entries may overwrite handcrafted project descriptions, featured flags, or custom ordering set by the user. | Read the target file first. Detect non-standard structure. For `featured: true` entries, preserve all fields. Only upsert new or changed repos. |
| Capturing screenshots for every repo indiscriminately | Repos without live demos waste Playwright launch time (~3s each). Repos with broken URLs produce useless error screenshots. | Pre-filter: only screenshot repos where `homepage` field is non-empty and returns 200 on a HEAD request. |
| Running sync on a metered or slow connection | Cloning or screenshotting dozens of repos can consume significant bandwidth and time. | Run in batches of 10. Add a `--dry-run` flag that lists what would change without executing. |
| Using the Supabase anon key instead of service_role key for writes | Anon key has row-level security restrictions that block upserts. | Use service_role key with caution. Store in environment variable, never in code. Add table-level RLS policies for the sync function. |
| Not cleaning up Playwright browser processes after sync | Each `chromium.launch()` spawns a process. Orphaned processes accumulate memory and can exhaust system resources. | Always call `browser.close()` in a `finally` block. Track launched browsers in an array and force-close on SIGINT/SIGTERM. |
| Hardcoding the portfolio data path instead of reading from config | Paths change between projects. A hardcoded path breaks on first use on a different machine. | Ask for portfolio directory in Step 1. Derive `projects-data.js` path from that base. Store in sync state for reuse. |

More from EliasOulkadi/shokunin

SkillDescription
Aesthetic Web Design>
agent-toolsRun 150+ AI apps via inference.sh CLI - image generation, video creation, LLMs, search, 3D, Twitter automation. Models: FLUX, Veo, Gemini, Grok, Claude, Seedance, OmniHuman, Tavily, Exa, OpenRouter, and many more. Use when running AI apps, generating images/videos, calling LLMs, web search, or automating Twitter. Triggers: inference.sh, infsh, ai model, run ai, serverless ai, ai api, flux, veo, claude api, image generation, video generation, openrouter, tavily, exa search, twitter api, grok
api-forgeDesign REST/GraphQL APIs with OpenAPI 3.1, error handling, pagination, rate limiting, webhooks, and idempotency. Use when user asks to design an API, create endpoints, define REST/GraphQL schema, or generate OpenAPI spec. Do NOT use for database schema design, frontend API integration, or non-HTTP protocols (gRPC, WebSocket, MQTT).
auth-architectImplement authentication and authorization with OWASP Top 10 standards, OAuth 2.0 + OIDC, WebAuthn/Passkeys, session management, and RBAC/ABAC. Use when user asks to implement login, signup, authentication, authorization, JWT, OAuth, SSO, passkeys, MFA, or role-based access. Do NOT use for API key management (use api-forge), encryption at rest, or network-level security (firewalls, WAF).
business-proposalsGenerate sales outreach sequences, proposals, SOWs, investor pitch decks, and RFP responses. Covers cold email sequences, proposal structure, pricing tiers (Good-Better-Best), scope of work with exclusions, and 10-slide pitch deck.
ci-cdDesign CI/CD pipelines for GitHub Actions, GitLab CI, and CircleCI with matrix builds, test sharding, caching, Docker layer caching, OIDC auth, deployment strategies (rolling, blue-green, canary), auto-rollback, self-hosted runners, and environment protection with manual approvals. Use when user asks to set up CI/CD, write a pipeline, configure GitHub Actions/GitLab CI/CircleCI, automate deployments, or set up build/test/deploy workflows. Do NOT use for Dockerfile authoring (use docker), K8s manifests (use kubernetes), or Terraform config (use terraform).
communication>
component-forgeBuild production-grade components for React, Vue 3, and Svelte 5 with all states (loading, empty, error, success, idle), TypeScript strict, WCAG 2.2 accessibility, server components (RSC), and compound component patterns. Includes scaffold script, reference patterns, and template files for React and Vue. Use when user asks to create a UI component, frontend module, or design system component. Do NOT use for page layouts (use landing-craft), routing, or state management architecture (global stores).
comprehensive-reviewComprehensive code review using parallel specialized subagents. If a PR URL is provided, fetches PR details and can post comments. If no PR is provided, reviews the diff between the current branch and its base branch plus any uncommitted changes. CRITICAL: this skill is costly, don't use it unless user explicitly requested to use it.
cross-reviewDelegate code review to a subagent running a specific model. Use ONLY when user explicitly names a model to review changes ("review with opus", "use sonnet to review", "review with gemini"). The root agent reconstructs changes from conversation history and spawns a subagent with the code-review skill using the specified model. Do NOT use for general code review (use code-review skill instead), for reviewing PRs from git history, or when no model is specified by the user.