Llevas tres meses construyendo una librería que resuelve un problema real. Tests completos, documentación, ejemplos de uso. La publicas en GitHub y NPM. Durante la primera semana está arriba en tu perfil porque es lo último que actualizaste. Recibes algunas estrellas, forks, issues. Todo va bien.
Entonces haces un hotfix de un typo en un repo de hace dos años. O creas un repo temporal para probar algo. O simplemente actualizas el README de otro proyecto. Vuelves a tu perfil de GitHub y tu proyecto estrella ya no está visible. Está enterrado debajo de repos que nadie visita.
El problema es que GitHub ordena repositorios por fecha de última actualización. No por estrellas, no por forks, no por importancia. El repo que tocaste hace cinco minutos aparece primero. El que lleva tres semanas sin commits desaparece del radar aunque tenga mil estrellas.
Para cualquiera que use GitHub como escaparate, esto es un problema. Un desarrollador que busca trabajo quiere que sus mejores proyectos estén visibles cuando un reclutador mira su perfil. Una empresa que mantiene repos públicos quiere que sus productos principales aparezcan primero, no el repo de configuración de CI que tocaron ayer. Un mantenedor de open source quiere destacar las librerías activas, no los forks experimentales.
Sí, GitHub tiene la opción de "pinned repositories" que te permite fijar hasta seis repos en tu perfil. Pero solo seis. Y los pinned no controlan el orden del resto de repos que aparecen debajo. Si tienes veinte proyectos relevantes, los catorce que no caben en pinned siguen ordenándose por fecha. Quería control total sobre el orden, no solo sobre los primeros seis.
La solución obvia no funciona
La primera idea es simplemente tocar los repos importantes de vez en cuando. Abrir cada uno, editar algo, hacer commit. Pero esto tiene problemas obvios: es manual, tedioso, y si tienes diez proyectos importantes, significa diez commits basura cada vez que quieras reordenar.
La segunda idea es automatizar de alguna forma. Pero GitHub no tiene API para cambiar el orden de repos directamente. El orden se deriva de la fecha de última actualización, punto.
El truco: commits vacíos
Lo que sí puedes hacer es actualizar el timestamp de "última actualización" sin modificar código. Git permite commits vacíos con el flag --allow-empty. Un commit vacío no cambia ningún archivo, pero sí actualiza la fecha del repo.
git commit --allow-empty -m "bump"
git push
Este commit aparece en el historial pero no tiene diff. El repo ahora tiene fecha de última actualización de hace un segundo, así que sube al principio de la lista.
El problema es que el historial se llena de commits "bump" que no significan nada. Para solucionarlo, puedes revertir inmediatamente:
git commit --allow-empty -m "gitpins: bump"
git revert HEAD --no-edit
git push
Ahora tienes dos commits: uno vacío y su revert. El historial sigue limpio porque el estado final es idéntico al inicial. Pero el timestamp se actualizó.
Automatización con GitHub Actions
Hacer esto manualmente cada pocas horas no tiene sentido. La solución real es un workflow de GitHub Actions que se ejecuta periódicamente y llama a un endpoint que hace el trabajo.
El flujo es:
- Creas un repositorio de configuración (
gitpins-config) - Ese repo contiene un workflow que se ejecuta cada N horas
- El workflow tiene un secret que identifica tu configuración
- Cuando se ejecuta, hace una llamada HTTP al endpoint de sync
- El endpoint itera sobre tus repos y hace los commits vacíos en orden inverso
- El último repo procesado queda con el timestamp más reciente, así que aparece primero
El workflow YAML real que genera GitPins:
name: GitPins - Maintain Repo Order
on:
schedule:
- cron: '0 */6 * * *' # Cada 6 horas (configurable)
workflow_dispatch: # Ejecución manual
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Trigger GitPins Sync
run: |
curl -s -X POST "https://gitpins.vercel.app/api/sync/${{ secrets.GITPINS_SYNC_SECRET }}" \
-H "Content-Type: application/json" \
-o response.json
echo "Response:"
cat response.json
if grep -q '"success":true' response.json; then
echo "Sync completed successfully!"
else
echo "Sync may have had issues, check response above"
fi
La frecuencia del cron es configurable: cada 1, 2, 4, 6, 8, 12, o 24 horas. El secret GITPINS_SYNC_SECRET es un UUID que GitPins genera automáticamente y configura en el repo.
Por qué monté GitPins
Implementar esto manualmente es factible pero tedioso. Tienes que crear el repo de configuración, escribir el workflow YAML, generar el token con los permisos correctos, configurar los secrets, y mantener la lista de repos actualizada.
GitPins automatiza todo esto con una interfaz web. Te conectas con GitHub OAuth, ves tus repos en una lista, los arrastras al orden que quieras, y activas la sincronización. GitPins crea el repo de configuración con el workflow automáticamente, genera el secret necesario, y configura todo.
El dashboard te permite:
- Ver todos tus repos públicos y privados
- Ordenar con drag & drop
- Elegir la estrategia de commit (revert o branch)
- Configurar la frecuencia de sincronización (1, 2, 4, 6, 8, 12, o 24 horas)
- Incluir o excluir repos privados
- Ver el estado de la última sincronización
Arquitectura técnica
GitPins está construido con Next.js 15 usando App Router. La autenticación usa GitHub OAuth a través de una GitHub App, lo que permite solicitar permisos granulares.
src/
├── app/
│ ├── api/
│ │ ├── auth/ # OAuth flow
│ │ ├── repos/ # Listar y ordenar repos
│ │ ├── config/ # Crear repo de configuración
│ │ └── sync/ # Endpoint que llama el Action
│ ├── dashboard/ # UI principal
│ └── admin/ # Panel de administración
├── lib/
│ ├── crypto.ts # AES-256-GCM para tokens
│ ├── session.ts # JWT sessions
│ ├── github.ts # OAuth helpers
│ └── github-app.ts # Operaciones de GitHub App
La base de datos es PostgreSQL con Prisma. Guarda usuarios, sus preferencias de orden, tokens encriptados, y logs de sincronización.
Los tokens de GitHub se encriptan con AES-256-GCM antes de guardarse. La clave de encriptación está en una variable de entorno, nunca en el código ni en la base de datos.
Las dos estrategias de commit
GitPins soporta dos estrategias para actualizar el timestamp. Ambas usan la API de Git de GitHub directamente, sin necesidad de clonar repos:
Estrategia Revert (recomendada):
// Crear commit vacío
const { data: newCommit } = await octokit.rest.git.createCommit({
owner,
repo,
message: `[GitPins] Position: ${position}/${total}`,
tree: commitData.tree.sha, // Mismo tree = sin cambios
parents: [sha],
})
// Actualizar referencia
await octokit.rest.git.updateRef({
owner,
repo,
ref: `heads/${defaultBranch}`,
sha: newCommit.sha,
})
// Revertir inmediatamente
const { data: revertCommit } = await octokit.rest.git.createCommit({
owner,
repo,
message: '[GitPins] Revert',
tree: commitData.tree.sha, // Vuelve al tree original
parents: [newCommit.sha],
})
await octokit.rest.git.updateRef({
owner,
repo,
ref: `heads/${defaultBranch}`,
sha: revertCommit.sha,
})
Crea dos commits que se cancelan mutuamente. El historial queda limpio porque el tree final es idéntico al inicial. Es la opción por defecto.
Estrategia Branch:
const tempBranch = `gitpins-${Date.now()}`
// Crear branch temporal
await octokit.rest.git.createRef({
owner,
repo,
ref: `refs/heads/${tempBranch}`,
sha: sha,
})
// Commit vacío en el branch temporal
const { data: newCommit } = await octokit.rest.git.createCommit({
owner,
repo,
message: `[GitPins] Sync position: ${position}/${total}`,
tree: commitData.tree.sha,
parents: [sha],
})
await octokit.rest.git.updateRef({
owner,
repo,
ref: `heads/${tempBranch}`,
sha: newCommit.sha,
})
// Merge a main
await octokit.rest.repos.merge({
owner,
repo,
base: defaultBranch,
head: tempBranch,
commit_message: `[GitPins] Position: ${position}/${total}`,
})
// Borrar branch temporal
await octokit.rest.git.deleteRef({
owner,
repo,
ref: `heads/${tempBranch}`,
})
Crea una rama temporal, hace el commit ahí, mergea a main, y borra la rama. Genera un merge commit en el historial.
Seguridad y permisos
GitPins usa una GitHub App en lugar de OAuth tradicional porque las Apps permiten permisos más granulares.
Los permisos que solicita son:
- Contents (read/write): Para hacer los commits vacíos
- Metadata (read): Para listar repos
- Actions (read/write): Para crear el workflow
- Secrets (read/write): Para configurar el token de sync
Lo que GitPins no puede hacer:
- Leer tu código fuente (los commits son vacíos)
- Modificar archivos existentes
- Borrar repos o branches
- Acceder a otros datos de GitHub (issues, PRs, etc.)
Los tokens se encriptan antes de guardarse. Si alguien accede a la base de datos, ve blobs encriptados, no tokens usables.
El panel de administración
GitPins incluye un panel de admin para gestionar usuarios:
- Estadísticas: usuarios totales, activos, baneados, sincronizaciones totales
- Gestión de usuarios: ver todos los usuarios con sus repos de configuración
- Ban/unban: suspender usuarios que abusen del servicio
- Gráficos de actividad: registros y sincronizaciones de los últimos 30 días
El acceso se controla con la variable de entorno ADMIN_GITHUB_ID. Solo el usuario con ese ID de GitHub puede acceder a /admin.
# Obtener tu GitHub ID
curl https://api.github.com/users/TU_USERNAME | grep '"id"'
Self-hosting
Si prefieres hostear tu propia instancia, el proceso es:
- Crear una GitHub App en tu cuenta
- Configurar los permisos necesarios
- Clonar el repo y configurar
.env - Crear una base de datos PostgreSQL (Neon tiene tier gratuito)
- Ejecutar
npx prisma db push - Desplegar en Vercel o cualquier plataforma que soporte Next.js
Las variables de entorno necesarias:
DATABASE_URL="postgresql://..."
GITHUB_APP_ID="..."
GITHUB_APP_CLIENT_ID="..."
GITHUB_APP_CLIENT_SECRET="..."
GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----..."
NEXT_PUBLIC_APP_URL="https://tu-dominio.com"
JWT_SECRET="..."
ENCRYPTION_SECRET="..."
ADMIN_GITHUB_ID="..."
El README tiene instrucciones detalladas para crear la GitHub App con los permisos correctos y configurar las URLs de callback.
Casos de uso
Desarrolladores buscando trabajo: Tu perfil de GitHub es tu portfolio. Tener tus proyectos más impresionantes siempre visibles aumenta las posibilidades de que un reclutador vea tu mejor trabajo primero.
Mantenedores de múltiples librerías: Si mantienes varias librerías open source, puedes priorizar las que tienen más uso o las que quieres promocionar actualmente.
Organizadores de repos por contexto: Puedes agrupar repos relacionados manteniéndolos juntos en el orden. Proyectos de trabajo arriba, proyectos personales después, experimentos al final.
Automatización de portfolio: En lugar de revisar tu perfil cada semana para reordenar manualmente, GitPins lo hace automáticamente según tu configuración.
Stack técnico
- Framework: Next.js 15 (App Router)
- Lenguaje: TypeScript
- Base de datos: PostgreSQL vía Prisma
- Auth: GitHub OAuth (GitHub Apps)
- Estilos: Tailwind CSS
- Drag & Drop: dnd-kit
- Deploy: Vercel
El código fuente está en github.com/686f6c61/gitpins. La demo pública está en gitpins.vercel.app.
El proyecto es MIT license, así que puedes forkearlo, modificarlo, y hostearlo sin restricciones.